@markii/core 0.8.1 → 0.10.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.
Files changed (2) hide show
  1. package/dist/parse.js +97 -1
  2. 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
- return processor.parse(text);
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.8.1",
3
+ "version": "0.10.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",