@markii/core 0.8.0 → 0.9.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/dist/parse.js +97 -1
- package/package.json +1 -1
package/dist/parse.js
CHANGED
|
@@ -3,6 +3,7 @@ import remarkParse from 'remark-parse';
|
|
|
3
3
|
import remarkDirective from 'remark-directive';
|
|
4
4
|
import remarkFrontmatter from 'remark-frontmatter';
|
|
5
5
|
import remarkGfm from 'remark-gfm';
|
|
6
|
+
import { visit } from 'unist-util-visit';
|
|
6
7
|
/**
|
|
7
8
|
* Parses Mark text into an mdast AST. CommonMark plus GitHub-Flavored
|
|
8
9
|
* Markdown (`remark-gfm`: tables, task lists, strikethrough, autolinks) plus
|
|
@@ -36,5 +37,100 @@ export function parse(text) {
|
|
|
36
37
|
.use(remarkFrontmatter)
|
|
37
38
|
.use(remarkDirective)
|
|
38
39
|
.use(remarkGfm);
|
|
39
|
-
|
|
40
|
+
const tree = processor.parse(text);
|
|
41
|
+
demoteInvalidTextDirectives(tree, text);
|
|
42
|
+
return tree;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `remark-directive` treats every single colon as a potential text
|
|
46
|
+
* directive, with no notion of ordinary prose. That misreads a clock time
|
|
47
|
+
* (`12:34`), a Bible verse (`John 3:16`), a ratio (`a:b`), or a `key:value`
|
|
48
|
+
* pair as a directive, and every renderer then shows an "unknown component"
|
|
49
|
+
* fallback box in the middle of an otherwise plain sentence — something a
|
|
50
|
+
* casual user hits constantly and has no way to escape short of learning
|
|
51
|
+
* directive syntax.
|
|
52
|
+
*
|
|
53
|
+
* This pass narrows recognition to directives that actually look
|
|
54
|
+
* intentional: a text directive is kept only when its name starts with an
|
|
55
|
+
* ASCII letter (`kbd`, not `34`) AND the colon sits at a word start — either
|
|
56
|
+
* the very first character of the document, or immediately preceded by
|
|
57
|
+
* something other than an ASCII letter/digit. Whitespace and punctuation
|
|
58
|
+
* (`(`, `*`, `_`, `"`, `>`, a newline) all count as a word start; a letter
|
|
59
|
+
* or digit does not. This is checked against the raw source text via the
|
|
60
|
+
* node's own start offset (not sibling nodes), so it works across emphasis
|
|
61
|
+
* boundaries too — `**:badge[x]**` keeps the directive (`*` precedes the
|
|
62
|
+
* colon) while `word:kbd[x]` demotes (`d` precedes it).
|
|
63
|
+
*
|
|
64
|
+
* A textDirective that fails the rule is replaced with a plain `text` node
|
|
65
|
+
* holding the exact original source slice, so the sentence reads exactly as
|
|
66
|
+
* written. Leaf (`::`) and container (`:::`) directives are untouched: they
|
|
67
|
+
* only ever occur at the start of a line, so they can't collide with prose
|
|
68
|
+
* colons in the first place.
|
|
69
|
+
*/
|
|
70
|
+
function demoteInvalidTextDirectives(tree, source) {
|
|
71
|
+
visit(tree, 'textDirective', (node, index, parent) => {
|
|
72
|
+
if (parent === undefined || index === undefined)
|
|
73
|
+
return;
|
|
74
|
+
const directive = node;
|
|
75
|
+
const position = directive.position;
|
|
76
|
+
if (position === undefined ||
|
|
77
|
+
position.start.offset === undefined ||
|
|
78
|
+
position.end.offset === undefined) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (isRecognizedTextDirective(directive.name, position.start.offset, source)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const replacement = {
|
|
85
|
+
type: 'text',
|
|
86
|
+
value: source.slice(position.start.offset, position.end.offset),
|
|
87
|
+
position,
|
|
88
|
+
};
|
|
89
|
+
parent.children[index] = replacement;
|
|
90
|
+
});
|
|
91
|
+
mergeAdjacentText(tree);
|
|
92
|
+
}
|
|
93
|
+
/** ASCII letter check for the directive-name start (`kbd`, not `34` or `1st`). */
|
|
94
|
+
const STARTS_WITH_LETTER = /^[A-Za-z]/;
|
|
95
|
+
/** ASCII letter-or-digit check for the character immediately before the colon. */
|
|
96
|
+
const IS_WORD_CHARACTER = /[A-Za-z0-9]/;
|
|
97
|
+
function isRecognizedTextDirective(name, colonOffset, source) {
|
|
98
|
+
if (!STARTS_WITH_LETTER.test(name))
|
|
99
|
+
return false;
|
|
100
|
+
if (colonOffset === 0)
|
|
101
|
+
return true;
|
|
102
|
+
const charBefore = source[colonOffset - 1];
|
|
103
|
+
return charBefore === undefined || !IS_WORD_CHARACTER.test(charBefore);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Merges runs of adjacent `text` sibling nodes back into one, in place,
|
|
107
|
+
* recursively over the whole tree. Needed after `demoteInvalidTextDirectives`
|
|
108
|
+
* replaces a textDirective with a text node: the paragraph
|
|
109
|
+
* `"Meet at 12:34 pm sharp."` must come back as ONE contiguous text node
|
|
110
|
+
* (matching what a plain-prose paragraph looks like everywhere else in the
|
|
111
|
+
* corpus), not three fragments split around where a directive used to be.
|
|
112
|
+
*/
|
|
113
|
+
function mergeAdjacentText(node) {
|
|
114
|
+
if (!hasChildren(node))
|
|
115
|
+
return;
|
|
116
|
+
const children = node.children;
|
|
117
|
+
const merged = [];
|
|
118
|
+
for (const child of children) {
|
|
119
|
+
const last = merged[merged.length - 1];
|
|
120
|
+
if (last !== undefined && last.type === 'text' && child.type === 'text') {
|
|
121
|
+
last.value += child.value;
|
|
122
|
+
if (last.position !== undefined && child.position !== undefined) {
|
|
123
|
+
last.position = { start: last.position.start, end: child.position.end };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
merged.push(child);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
node.children = merged;
|
|
131
|
+
for (const child of merged)
|
|
132
|
+
mergeAdjacentText(child);
|
|
133
|
+
}
|
|
134
|
+
function hasChildren(node) {
|
|
135
|
+
return Array.isArray(node.children);
|
|
40
136
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Framework-agnostic reference parser for Mark (.mk.md): CommonMark + directives to mdast, and mdast to a sanitized hast tree. Zero React dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|