@markset-lang/remark-markset 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +114 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
- package/src/index.ts +155 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @markset-lang/remark-markset
|
|
2
|
+
|
|
3
|
+
A [remark](https://github.com/remarkjs/remark) plugin that teaches an existing [unified](https://unifiedjs.com) pipeline to read [Markset](https://markset-lang.github.io/markset/).
|
|
4
|
+
|
|
5
|
+
If you already run remark — in Astro, Next, Eleventy, Gatsby, a lint step, a script — this is how you get Markset's constructs without replacing any of it.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i @markset-lang/remark-markset
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Parsing
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { unified } from "unified";
|
|
15
|
+
import remarkParse from "remark-parse";
|
|
16
|
+
import remarkMarkset from "@markset-lang/remark-markset";
|
|
17
|
+
|
|
18
|
+
const tree = await unified().use(remarkParse).use(remarkMarkset).parse(source);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Constructs become typed mdast nodes — `grid`, `card`, `callout`, `figure`, `tabs`, `steps`, `metrics`, `columns` — alongside `span` nodes for bracketed spans. Attribute specifiers and attribute lines are lowered onto `data.hProperties`, so any mdast-to-hast conversion picks them up with no further work.
|
|
22
|
+
|
|
23
|
+
## Rendering to HTML
|
|
24
|
+
|
|
25
|
+
The renderer's handlers are a separate package, so you only install them if you want them:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import remarkRehype from "remark-rehype";
|
|
29
|
+
import rehypeStringify from "rehype-stringify";
|
|
30
|
+
import { marksetHandlers } from "@markset-lang/render-html";
|
|
31
|
+
|
|
32
|
+
const file = await unified()
|
|
33
|
+
.use(remarkParse)
|
|
34
|
+
.use(remarkMarkset)
|
|
35
|
+
.use(remarkRehype, { handlers: marksetHandlers() })
|
|
36
|
+
.use(rehypeStringify)
|
|
37
|
+
.process(source);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
That pipeline produces the same HTML as the library's own `renderHtml`, and a test in this repository asserts it byte for byte.
|
|
41
|
+
|
|
42
|
+
You will also want the stylesheet, which is `@markset-lang/render-html/css/markset.css`, or `markset css` from the CLI.
|
|
43
|
+
|
|
44
|
+
## Diagnostics
|
|
45
|
+
|
|
46
|
+
Markset has a closed vocabulary, so an unknown directive name is an error rather than silent passthrough (spec §3). Those arrive as ordinary vfile messages:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
for (const m of file.messages) {
|
|
50
|
+
console.log(m.fatal ? "error" : "warning", m.ruleId, m.reason, m.line, m.column);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`ruleId` is the Markset diagnostic code (`DIRECTIVE_UNKNOWN_NAME`, `GRID_BAD_COLS`, …) and `source` is `"markset"`, so a pipeline that already reports vfile messages will report these without being taught anything.
|
|
55
|
+
|
|
56
|
+
`fatal` is `true` for errors and `false` for warnings. A document with any error is invalid; warnings are not.
|
|
57
|
+
|
|
58
|
+
## What it deliberately does not turn on
|
|
59
|
+
|
|
60
|
+
This plugin adds Markset and nothing else, because your pipeline already has opinions:
|
|
61
|
+
|
|
62
|
+
- **Tables** are GFM. Add `remark-gfm`.
|
|
63
|
+
- **Frontmatter** is `remark-frontmatter`. Add it, and this plugin will read the `yaml` node it produces into the document's theme (spec §6). Without it, frontmatter is not parsed and nothing breaks.
|
|
64
|
+
|
|
65
|
+
The reference `parseDocument` enables both because it is rendering whole documents; a plugin that did the same would be changing your pipeline behind your back.
|
|
66
|
+
|
|
67
|
+
## Every CommonMark document is unaffected
|
|
68
|
+
|
|
69
|
+
Markset is a strict superset: a document with no constructs in it parses and renders exactly as it did before you added the plugin. There is a test for precisely that, comparing the output with and without.
|
|
70
|
+
|
|
71
|
+
## Peer dependencies
|
|
72
|
+
|
|
73
|
+
`unified` and `remark-parse`, both version 11 or later — you already have them.
|
|
74
|
+
|
|
75
|
+
MIT.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A remark plugin that teaches an existing unified pipeline to read Markset.
|
|
3
|
+
*
|
|
4
|
+
* Markset's parser is already micromark and mdast underneath, so this is
|
|
5
|
+
* assembly rather than a second implementation: the same syntax extension, the
|
|
6
|
+
* same mdast compiler extension, and the same normalization and validation
|
|
7
|
+
* that `parseDocument` runs, arranged the way unified expects them.
|
|
8
|
+
*
|
|
9
|
+
* What it deliberately does not do is turn on anything the caller did not ask
|
|
10
|
+
* for. `parseDocument` enables GFM tables and YAML frontmatter because a
|
|
11
|
+
* Markset document may use both; a pipeline already has its own opinion about
|
|
12
|
+
* those, so add `remark-gfm` and `remark-frontmatter` if you want them. This
|
|
13
|
+
* plugin reads frontmatter when something else has parsed it into a `yaml`
|
|
14
|
+
* node, and ignores its absence.
|
|
15
|
+
*/
|
|
16
|
+
import type { Root } from "mdast";
|
|
17
|
+
import type { Plugin } from "unified";
|
|
18
|
+
declare const remarkMarkset: Plugin<[], Root, Root>;
|
|
19
|
+
export default remarkMarkset;
|
|
20
|
+
export { remarkMarkset };
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAS,IAAI,EAAE,MAAM,OAAO,CAAC;AAMzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAoBtC,QAAA,MAAM,aAAa,EAAE,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAoDzC,CAAC;eA4Da,aAAa;AAC5B,OAAO,EAAE,aAAa,EAAE,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { attachAttributeLines, markset, marksetFromMarkdown, normalizeConstructs, readFrontmatter, validateStructure, } from "@markset-lang/parser";
|
|
2
|
+
/** Diagnostics collected during parsing, parked on the tree until the transform runs. */
|
|
3
|
+
const CARRIED = "_marksetDiagnostics";
|
|
4
|
+
const remarkMarkset = function remarkMarkset() {
|
|
5
|
+
const data = this.data();
|
|
6
|
+
data.micromarkExtensions ??= [];
|
|
7
|
+
data.fromMarkdownExtensions ??= [];
|
|
8
|
+
const micromarkExtensions = data.micromarkExtensions;
|
|
9
|
+
const fromMarkdownExtensions = data.fromMarkdownExtensions;
|
|
10
|
+
// The mdast extension reports into an array, and it is built once while
|
|
11
|
+
// parsing happens once per file. Handing it a long-lived array and reading
|
|
12
|
+
// that array later would mix two files' diagnostics together the first time
|
|
13
|
+
// anything processed two at once. A from-markdown transform runs
|
|
14
|
+
// synchronously at the end of the parse that produced the tree, so moving
|
|
15
|
+
// the diagnostics onto the tree there ties them to their own document and
|
|
16
|
+
// leaves the array empty for the next one.
|
|
17
|
+
const collected = [];
|
|
18
|
+
const extension = marksetFromMarkdown(collected);
|
|
19
|
+
const transforms = [
|
|
20
|
+
...(extension.transforms ?? []),
|
|
21
|
+
(tree) => {
|
|
22
|
+
tree[CARRIED] = collected.splice(0);
|
|
23
|
+
return undefined;
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
micromarkExtensions.push(markset());
|
|
27
|
+
fromMarkdownExtensions.push({ ...extension, transforms });
|
|
28
|
+
return (tree, file) => {
|
|
29
|
+
const source = String(file);
|
|
30
|
+
const carrier = tree;
|
|
31
|
+
const diagnostics = carrier[CARRIED] ?? [];
|
|
32
|
+
delete carrier[CARRIED];
|
|
33
|
+
const first = tree.children[0];
|
|
34
|
+
const meta = first?.type === "yaml" ? readFrontmatter(first, diagnostics) : null;
|
|
35
|
+
if (meta)
|
|
36
|
+
tree.frontmatter = meta;
|
|
37
|
+
attachAttributeLines(tree, source, diagnostics);
|
|
38
|
+
diagnostics.push(...validateStructure(tree, source));
|
|
39
|
+
normalizeConstructs(tree, source, diagnostics);
|
|
40
|
+
applyAttributes(tree);
|
|
41
|
+
diagnostics.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
42
|
+
for (const d of diagnostics) {
|
|
43
|
+
const message = file.message(d.message, { place: pointAt(source, d.start), ruleId: d.code, source: "markset" });
|
|
44
|
+
// A vfile message is a warning unless it says otherwise, and Markset
|
|
45
|
+
// draws that line itself: a document with an error is invalid (§7), so
|
|
46
|
+
// the two severities must not collapse into one here.
|
|
47
|
+
message.fatal = d.severity === "error";
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Node types whose own to-hast handler reads `attributes` itself, so writing
|
|
54
|
+
* hProperties for them would say the same thing twice. The list is the one in
|
|
55
|
+
* render-html; a test renders the same document both ways and fails if the two
|
|
56
|
+
* paths stop agreeing, which is what keeps this copy honest.
|
|
57
|
+
*/
|
|
58
|
+
const HANDLED = new Set([
|
|
59
|
+
"callout",
|
|
60
|
+
"card",
|
|
61
|
+
"grid",
|
|
62
|
+
"columns",
|
|
63
|
+
"column",
|
|
64
|
+
"tabs",
|
|
65
|
+
"tab",
|
|
66
|
+
"steps",
|
|
67
|
+
"metrics",
|
|
68
|
+
"figure",
|
|
69
|
+
"span",
|
|
70
|
+
"directive",
|
|
71
|
+
"separator",
|
|
72
|
+
]);
|
|
73
|
+
/**
|
|
74
|
+
* Lower §2.1 and §2.5 attributes onto `data.hProperties`, which is how mdast
|
|
75
|
+
* carries them to any hast conversion. Doing it here rather than inside a
|
|
76
|
+
* renderer is what makes an attribute line work with plain `remark-rehype`,
|
|
77
|
+
* and with anything else downstream that reads mdast.
|
|
78
|
+
*/
|
|
79
|
+
function applyAttributes(tree) {
|
|
80
|
+
const visit = (node) => {
|
|
81
|
+
const attributes = node.attributes;
|
|
82
|
+
if (attributes && !HANDLED.has(node.type)) {
|
|
83
|
+
const properties = {};
|
|
84
|
+
if (attributes.id)
|
|
85
|
+
properties.id = attributes.id;
|
|
86
|
+
if (attributes.classes.length)
|
|
87
|
+
properties.className = attributes.classes;
|
|
88
|
+
for (const [key, value] of Object.entries(attributes.attrs))
|
|
89
|
+
properties[`data-${key}`] = value;
|
|
90
|
+
node.data = {
|
|
91
|
+
...node.data,
|
|
92
|
+
hProperties: { ...node.data?.hProperties, ...properties },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if ("children" in node)
|
|
96
|
+
for (const child of node.children)
|
|
97
|
+
visit(child);
|
|
98
|
+
};
|
|
99
|
+
visit(tree);
|
|
100
|
+
}
|
|
101
|
+
function pointAt(source, offset) {
|
|
102
|
+
let line = 1;
|
|
103
|
+
let lineStart = 0;
|
|
104
|
+
for (let i = 0; i < offset && i < source.length; i++) {
|
|
105
|
+
if (source[i] === "\n") {
|
|
106
|
+
line++;
|
|
107
|
+
lineStart = i + 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { line, column: offset - lineStart + 1, offset };
|
|
111
|
+
}
|
|
112
|
+
export default remarkMarkset;
|
|
113
|
+
export { remarkMarkset };
|
|
114
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@markset-lang/remark-markset",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "remark plugin: parse Markset constructs in an existing unified pipeline.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.18"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/markset-lang/markset.git",
|
|
13
|
+
"directory": "packages/remark-markset"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://markset-lang.github.io/markset/",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"remark",
|
|
21
|
+
"remark-plugin",
|
|
22
|
+
"unified",
|
|
23
|
+
"mdast",
|
|
24
|
+
"markset"
|
|
25
|
+
],
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"markset-source": "./src/index.ts",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@markset-lang/parser": "^0.3.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"remark-parse": ">=11",
|
|
42
|
+
"unified": ">=11"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A remark plugin that teaches an existing unified pipeline to read Markset.
|
|
3
|
+
*
|
|
4
|
+
* Markset's parser is already micromark and mdast underneath, so this is
|
|
5
|
+
* assembly rather than a second implementation: the same syntax extension, the
|
|
6
|
+
* same mdast compiler extension, and the same normalization and validation
|
|
7
|
+
* that `parseDocument` runs, arranged the way unified expects them.
|
|
8
|
+
*
|
|
9
|
+
* What it deliberately does not do is turn on anything the caller did not ask
|
|
10
|
+
* for. `parseDocument` enables GFM tables and YAML frontmatter because a
|
|
11
|
+
* Markset document may use both; a pipeline already has its own opinion about
|
|
12
|
+
* those, so add `remark-gfm` and `remark-frontmatter` if you want them. This
|
|
13
|
+
* plugin reads frontmatter when something else has parsed it into a `yaml`
|
|
14
|
+
* node, and ignores its absence.
|
|
15
|
+
*/
|
|
16
|
+
import type { Nodes, Root } from "mdast";
|
|
17
|
+
// For the type augmentation alone: remark-parse is what declares
|
|
18
|
+
// micromarkExtensions and fromMarkdownExtensions on unified's Data. Every
|
|
19
|
+
// remark plugin that registers a syntax extension does this, remark-gfm
|
|
20
|
+
// included. Nothing is imported at runtime.
|
|
21
|
+
import type {} from "remark-parse";
|
|
22
|
+
import type { Plugin } from "unified";
|
|
23
|
+
import type { VFile } from "vfile";
|
|
24
|
+
import {
|
|
25
|
+
type Attributes,
|
|
26
|
+
attachAttributeLines,
|
|
27
|
+
markset,
|
|
28
|
+
marksetFromMarkdown,
|
|
29
|
+
normalizeConstructs,
|
|
30
|
+
readFrontmatter,
|
|
31
|
+
validateStructure,
|
|
32
|
+
type Diagnostic,
|
|
33
|
+
} from "@markset-lang/parser";
|
|
34
|
+
|
|
35
|
+
/** Diagnostics collected during parsing, parked on the tree until the transform runs. */
|
|
36
|
+
const CARRIED = "_marksetDiagnostics";
|
|
37
|
+
|
|
38
|
+
interface Carrier {
|
|
39
|
+
[CARRIED]?: Diagnostic[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const remarkMarkset: Plugin<[], Root, Root> = function remarkMarkset() {
|
|
43
|
+
const data = this.data();
|
|
44
|
+
data.micromarkExtensions ??= [];
|
|
45
|
+
data.fromMarkdownExtensions ??= [];
|
|
46
|
+
const micromarkExtensions = data.micromarkExtensions;
|
|
47
|
+
const fromMarkdownExtensions = data.fromMarkdownExtensions;
|
|
48
|
+
|
|
49
|
+
// The mdast extension reports into an array, and it is built once while
|
|
50
|
+
// parsing happens once per file. Handing it a long-lived array and reading
|
|
51
|
+
// that array later would mix two files' diagnostics together the first time
|
|
52
|
+
// anything processed two at once. A from-markdown transform runs
|
|
53
|
+
// synchronously at the end of the parse that produced the tree, so moving
|
|
54
|
+
// the diagnostics onto the tree there ties them to their own document and
|
|
55
|
+
// leaves the array empty for the next one.
|
|
56
|
+
const collected: Diagnostic[] = [];
|
|
57
|
+
const extension = marksetFromMarkdown(collected);
|
|
58
|
+
const transforms = [
|
|
59
|
+
...(extension.transforms ?? []),
|
|
60
|
+
(tree: Root): undefined => {
|
|
61
|
+
(tree as Root & Carrier)[CARRIED] = collected.splice(0);
|
|
62
|
+
return undefined;
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
micromarkExtensions.push(markset());
|
|
67
|
+
fromMarkdownExtensions.push({ ...extension, transforms });
|
|
68
|
+
|
|
69
|
+
return (tree: Root, file: VFile): undefined => {
|
|
70
|
+
const source = String(file);
|
|
71
|
+
const carrier = tree as Root & Carrier;
|
|
72
|
+
const diagnostics = carrier[CARRIED] ?? [];
|
|
73
|
+
delete carrier[CARRIED];
|
|
74
|
+
|
|
75
|
+
const first = tree.children[0];
|
|
76
|
+
const meta = first?.type === "yaml" ? readFrontmatter(first, diagnostics) : null;
|
|
77
|
+
if (meta) tree.frontmatter = meta;
|
|
78
|
+
|
|
79
|
+
attachAttributeLines(tree, source, diagnostics);
|
|
80
|
+
diagnostics.push(...validateStructure(tree, source));
|
|
81
|
+
normalizeConstructs(tree, source, diagnostics);
|
|
82
|
+
applyAttributes(tree);
|
|
83
|
+
diagnostics.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
84
|
+
|
|
85
|
+
for (const d of diagnostics) {
|
|
86
|
+
const message = file.message(d.message, { place: pointAt(source, d.start), ruleId: d.code, source: "markset" });
|
|
87
|
+
// A vfile message is a warning unless it says otherwise, and Markset
|
|
88
|
+
// draws that line itself: a document with an error is invalid (§7), so
|
|
89
|
+
// the two severities must not collapse into one here.
|
|
90
|
+
message.fatal = d.severity === "error";
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Node types whose own to-hast handler reads `attributes` itself, so writing
|
|
98
|
+
* hProperties for them would say the same thing twice. The list is the one in
|
|
99
|
+
* render-html; a test renders the same document both ways and fails if the two
|
|
100
|
+
* paths stop agreeing, which is what keeps this copy honest.
|
|
101
|
+
*/
|
|
102
|
+
const HANDLED = new Set([
|
|
103
|
+
"callout",
|
|
104
|
+
"card",
|
|
105
|
+
"grid",
|
|
106
|
+
"columns",
|
|
107
|
+
"column",
|
|
108
|
+
"tabs",
|
|
109
|
+
"tab",
|
|
110
|
+
"steps",
|
|
111
|
+
"metrics",
|
|
112
|
+
"figure",
|
|
113
|
+
"span",
|
|
114
|
+
"directive",
|
|
115
|
+
"separator",
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Lower §2.1 and §2.5 attributes onto `data.hProperties`, which is how mdast
|
|
120
|
+
* carries them to any hast conversion. Doing it here rather than inside a
|
|
121
|
+
* renderer is what makes an attribute line work with plain `remark-rehype`,
|
|
122
|
+
* and with anything else downstream that reads mdast.
|
|
123
|
+
*/
|
|
124
|
+
function applyAttributes(tree: Root): void {
|
|
125
|
+
const visit = (node: Nodes): void => {
|
|
126
|
+
const attributes = (node as { attributes?: Attributes }).attributes;
|
|
127
|
+
if (attributes && !HANDLED.has(node.type)) {
|
|
128
|
+
const properties: Record<string, string | string[]> = {};
|
|
129
|
+
if (attributes.id) properties.id = attributes.id;
|
|
130
|
+
if (attributes.classes.length) properties.className = attributes.classes;
|
|
131
|
+
for (const [key, value] of Object.entries(attributes.attrs)) properties[`data-${key}`] = value;
|
|
132
|
+
node.data = {
|
|
133
|
+
...node.data,
|
|
134
|
+
hProperties: { ...(node.data as { hProperties?: object } | undefined)?.hProperties, ...properties },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if ("children" in node) for (const child of node.children) visit(child as Nodes);
|
|
138
|
+
};
|
|
139
|
+
visit(tree);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function pointAt(source: string, offset: number): { line: number; column: number; offset: number } {
|
|
143
|
+
let line = 1;
|
|
144
|
+
let lineStart = 0;
|
|
145
|
+
for (let i = 0; i < offset && i < source.length; i++) {
|
|
146
|
+
if (source[i] === "\n") {
|
|
147
|
+
line++;
|
|
148
|
+
lineStart = i + 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { line, column: offset - lineStart + 1, offset };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default remarkMarkset;
|
|
155
|
+
export { remarkMarkset };
|