@memorysmithapp/markdown-renderer 0.2.0 → 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 +18 -0
- package/dist/Markdown.d.ts +0 -6
- package/dist/Markdown.js +1 -31
- package/dist/NoteBody.js +5 -3
- package/dist/Transclusion.js +2 -2
- package/dist/diagnose.d.ts +50 -0
- package/dist/diagnose.js +304 -0
- package/dist/markdown.d.ts +20 -0
- package/dist/markdown.js +48 -0
- package/dist/reading.d.ts +6 -0
- package/dist/reading.js +32 -0
- package/dist/remark-ring.d.ts +8 -6
- package/dist/remark-ring.js +8 -28
- package/dist/tasklist.js +7 -3
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -74,6 +74,24 @@ const mine = defineTheme({ name: 'mine', components: { properties: () => null }
|
|
|
74
74
|
|
|
75
75
|
Import `structure.css` instead of `styles.css` for a theme that draws everything itself. A theme names faces; your page loads them.
|
|
76
76
|
|
|
77
|
+
## Diagnose
|
|
78
|
+
|
|
79
|
+
Every note is valid, and nothing here refuses one. What `diagnose` tells a host is where the page will not show what was most likely typed, and what the note lacks that the specification reserves a place for: the notices of [`SPEC.md` §7.13](https://github.com/memorysmithapp/memorysmithapp-markdown/blob/main/packages/markdown-spec/SPEC.md#713-notices). It imports no React and needs no DOM, so it runs in an editor, on a server, or in a tool that writes a note.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { diagnose } from '@memorysmithapp/markdown-renderer/diagnose';
|
|
83
|
+
|
|
84
|
+
const notices = await diagnose(note, {
|
|
85
|
+
resolve: (name) => addressOfTheOneNoteNamed(name), // or reaches; leave both out and no link is pending
|
|
86
|
+
keeps: (name) => notebookKeepsAFile(name),
|
|
87
|
+
});
|
|
88
|
+
// [{ code: 'heading-skipped-level', severity: 'notice', section: '3.3',
|
|
89
|
+
// message: 'This heading skips a level below the heading before it.',
|
|
90
|
+
// line: 12, column: 1, endLine: 12, endColumn: 14 }, …]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A `notice` says the page will differ from the text — a fence never closed, a formula KaTeX cannot typeset, a diagram Mermaid cannot read, raw HTML, a footnote with no definition or no reference, a single-tilde strikethrough, a frontmatter never closed, a `name` that leaves the note unnamed or none at all, `aliases` or `tags` that are not a list, a body that does not start at `#` or skips a level. A `hint` says nothing is drawn wrong: no `aliases`, no `tags`, a link or an embed no note answers yet. Lines and columns count from 1 over the whole note, frontmatter included, and the `message` is English; a host words it in its own language from the `code`, which is stable. Mermaid is loaded only for a note that holds a diagram, and `diagrams: false` skips it.
|
|
94
|
+
|
|
77
95
|
The host contract, the stylesheet's properties, the slots and the decisions behind them are in the repository's [`ARCHITECTURE.md`](https://github.com/memorysmithapp/memorysmithapp-markdown/blob/main/ARCHITECTURE.md).
|
|
78
96
|
|
|
79
97
|
## Licence
|
package/dist/Markdown.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type AnchorHTMLAttributes, type ReactNode } from 'react';
|
|
2
|
-
import type { PluggableList } from 'unified';
|
|
3
2
|
import { type FootnoteRef } from './footnotes.js';
|
|
4
3
|
/**
|
|
5
4
|
* One run of a note, parsed and drawn: the pipeline of MemorySmith's reading page.
|
|
@@ -38,10 +37,5 @@ type WithNode<T> = T & {
|
|
|
38
37
|
node?: unknown;
|
|
39
38
|
};
|
|
40
39
|
export declare function MarkdownAnchor({ href, children, node: _node, ...rest }: WithNode<AnchorHTMLAttributes<HTMLAnchorElement>>): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
|
|
41
|
-
/**
|
|
42
|
-
* The remark plugins of the reading surface, in their order. `planFootnotes` reads a note
|
|
43
|
-
* through the same list, so it counts the references this surface draws and no other.
|
|
44
|
-
*/
|
|
45
|
-
export declare function readingPlugins(titleOf: (kind: string) => string | undefined): PluggableList;
|
|
46
40
|
export declare function Markdown({ children, source, onToggleTask, writable, footnotes }: MarkdownProps): import("react").JSX.Element;
|
|
47
41
|
export {};
|
package/dist/Markdown.js
CHANGED
|
@@ -4,8 +4,6 @@ import { isValidElement, } from 'react';
|
|
|
4
4
|
import { Fragment, jsx, jsxs } from 'react/jsx-runtime';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import rehypeKatex from 'rehype-katex';
|
|
7
|
-
import remarkGfm from 'remark-gfm';
|
|
8
|
-
import remarkMath from 'remark-math';
|
|
9
7
|
import { followable } from './address.js';
|
|
10
8
|
import { CopyButton } from './CopyButton.js';
|
|
11
9
|
import { highlightLanguage, highlightTree } from './highlight.js';
|
|
@@ -14,8 +12,7 @@ import { readDimensions, readImageAlt } from './image-dimensions.js';
|
|
|
14
12
|
import { choiceTargetOf, LinkChoice } from './LinkChoice.js';
|
|
15
13
|
import { toUnixNewlines } from './markdown.js';
|
|
16
14
|
import { MermaidDiagram } from './MermaidDiagram.js';
|
|
17
|
-
import {
|
|
18
|
-
import { remarkBlockIds, remarkComments, remarkHighlight, remarkMathDollarRule } from './remark-ring.js';
|
|
15
|
+
import { readingPlugins } from './reading.js';
|
|
19
16
|
import { CLOBBER_PREFIX, rehypeFootnoteRun, rehypeFootnoteSection } from './footnotes.js';
|
|
20
17
|
import { drawSlot, headings, InBlock, MarkdownBlockquote, MarkdownDiv, MarkdownSection, MarkdownTable, useInBlock, useSlot } from './slots.js';
|
|
21
18
|
import { ordinalAt } from './tasklist.js';
|
|
@@ -251,33 +248,6 @@ function dropBox(children) {
|
|
|
251
248
|
return children;
|
|
252
249
|
return children.filter((child) => !(isValidElement(child) && child.type === 'input'));
|
|
253
250
|
}
|
|
254
|
-
/**
|
|
255
|
-
* The remark plugins of the reading surface, in their order. `planFootnotes` reads a note
|
|
256
|
-
* through the same list, so it counts the references this surface draws and no other.
|
|
257
|
-
*/
|
|
258
|
-
export function readingPlugins(titleOf) {
|
|
259
|
-
return [
|
|
260
|
-
/**
|
|
261
|
-
* `singleTilde: false` is a decision and not a default.
|
|
262
|
-
*
|
|
263
|
-
* GFM specifies strikethrough as `~~x~~`; GitHub also accepts a
|
|
264
|
-
* single tilde, outside its own specification. Here that extension
|
|
265
|
-
* actively misrenders a notation the specification leaves out:
|
|
266
|
-
* somebody writing `H~2~O` for a subscript would get `H` struck-out
|
|
267
|
-
* `2` `O`, which is a worse answer than nothing. With it off the
|
|
268
|
-
* characters stay on the page, and the author can see they got what
|
|
269
|
-
* the specification says they get, which is nothing.
|
|
270
|
-
*/
|
|
271
|
-
[remarkGfm, { singleTilde: false }],
|
|
272
|
-
[remarkCallouts, { titleOf }],
|
|
273
|
-
remarkHighlight,
|
|
274
|
-
remarkComments,
|
|
275
|
-
remarkBlockIds,
|
|
276
|
-
remarkMath,
|
|
277
|
-
// After remark-math, and it gives back what was never a formula.
|
|
278
|
-
remarkMathDollarRule,
|
|
279
|
-
];
|
|
280
|
-
}
|
|
281
251
|
export function Markdown({ children, source, onToggleTask, writable = false, footnotes }) {
|
|
282
252
|
const text = toUnixNewlines(children);
|
|
283
253
|
const { strings } = useHost();
|
package/dist/NoteBody.js
CHANGED
|
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
3
|
import { HOST_KEYS, HostProvider, resolveHost } from './host.js';
|
|
4
4
|
import { planFootnotes } from './footnotes.js';
|
|
5
|
-
import { Markdown
|
|
6
|
-
import {
|
|
5
|
+
import { Markdown } from './Markdown.js';
|
|
6
|
+
import { readingPlugins } from './reading.js';
|
|
7
|
+
import { bodyStart, hideComments, remoteImageHosts, resolveWikilinks } from './markdown.js';
|
|
7
8
|
import { readProperties } from './properties.js';
|
|
8
9
|
import { Properties } from './PropertiesBlock.js';
|
|
9
10
|
import { taskBoxes } from './tasklist.js';
|
|
@@ -25,7 +26,8 @@ export function NoteBody({ markdown, writable = false, onToggleTask, ...host })
|
|
|
25
26
|
// runs start after it. Everything below counts task items from there.
|
|
26
27
|
const split = bodyStart(markdown);
|
|
27
28
|
const properties = useMemo(() => readProperties(markdown), [markdown]);
|
|
28
|
-
|
|
29
|
+
// What a comment holds is never read (§7.6): it leaves before the embeds and the links are found.
|
|
30
|
+
const body = hideComments(markdown.slice(split)).text;
|
|
29
31
|
// The body is cut into runs of text and the embeds between them; each run
|
|
30
32
|
// is parsed on its own, as on MemorySmith's reading page.
|
|
31
33
|
const segments = splitEmbeds(body, resolved.keeps).map((segment) => segment.kind === 'text'
|
package/dist/Transclusion.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
|
|
|
3
3
|
import { useHost } from './host.js';
|
|
4
4
|
import { Markdown } from './Markdown.js';
|
|
5
5
|
import { drawSlot, useSlot } from './slots.js';
|
|
6
|
-
import { bodyStart, resolveWikilinks } from './markdown.js';
|
|
6
|
+
import { bodyStart, hideComments, resolveWikilinks } from './markdown.js';
|
|
7
7
|
import { blockOf, demoteEmbeds, isBlockAnchor, sectionOf } from './transclusion.js';
|
|
8
8
|
import { wikilinkAddress } from './wikilink-address.js';
|
|
9
9
|
/** The shape of what is coming, in the position it will occupy, while the host answers. */
|
|
@@ -49,7 +49,7 @@ export function Transclusion({ target, anchor }) {
|
|
|
49
49
|
return slot ? drawSlot(slot, { state: 'loading', target, anchor, caption: null, children: skeleton }) : skeleton;
|
|
50
50
|
}
|
|
51
51
|
const { note } = loaded;
|
|
52
|
-
const whole = note.markdown.slice(bodyStart(note.markdown));
|
|
52
|
+
const whole = hideComments(note.markdown.slice(bodyStart(note.markdown))).text;
|
|
53
53
|
// The caption names the note the content came from, and a note may have no
|
|
54
54
|
// name.
|
|
55
55
|
const sourceLabel = note.name ?? strings.unnamedNote;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The notices of a note (§7.13): where what is drawn will not be what was most likely typed.
|
|
3
|
+
*
|
|
4
|
+
* There is no invalid note (§6.4), so nothing here accepts or refuses: it reads a note the way the
|
|
5
|
+
* reading surface reads it — the same frontmatter cut, its comments taken out (`hideComments`), the
|
|
6
|
+
* same parser and the same remark plugins (`reading.ts`) — and reports, by a stable code, the places where the page will differ from the
|
|
7
|
+
* text, or where the note lacks something the specification reserves a place for. A notice never
|
|
8
|
+
* blocks a save and never changes a byte.
|
|
9
|
+
*
|
|
10
|
+
* It runs with no DOM and no React, so a host can call it on a server, from a tool that writes a
|
|
11
|
+
* note, or beside an editor. KaTeX checks a formula as `rehype-katex` would typeset it, and Mermaid
|
|
12
|
+
* reads a diagram as the page would draw it, loaded only when a note carries one.
|
|
13
|
+
*/
|
|
14
|
+
import type { NoteHost } from './host.js';
|
|
15
|
+
/** The codes of §7.13, which `spec.json` declares as `notices`. */
|
|
16
|
+
export type NoticeCode = 'fence-unclosed' | 'math-unclosed' | 'formula-invalid' | 'diagram-invalid' | 'frontmatter-unclosed' | 'name-missing' | 'name-unusable' | 'reserved-not-a-list' | 'aliases-missing' | 'tags-missing' | 'heading-first-level' | 'heading-skipped-level' | 'footnote-undefined' | 'footnote-unreferenced' | 'raw-html' | 'strikethrough-single-tilde' | 'link-pending' | 'embed-pending';
|
|
17
|
+
/** `notice`: the page will differ from what was typed. `hint`: nothing is drawn wrong. */
|
|
18
|
+
export type NoticeSeverity = 'notice' | 'hint';
|
|
19
|
+
/** One place in a note, by a code of §7.13. Lines and columns count from 1 over the whole note. */
|
|
20
|
+
export interface Notice {
|
|
21
|
+
readonly code: NoticeCode;
|
|
22
|
+
readonly severity: NoticeSeverity;
|
|
23
|
+
/** The section of the specification that decides what happens there. */
|
|
24
|
+
readonly section: string;
|
|
25
|
+
/** What a reader gets, in English; a host words it in its own language from `code`. */
|
|
26
|
+
readonly message: string;
|
|
27
|
+
readonly line: number;
|
|
28
|
+
readonly column: number;
|
|
29
|
+
readonly endLine: number;
|
|
30
|
+
readonly endColumn: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* What the notebook answers, as `NoteBody` asks it. With neither `resolve` nor `reaches`, nothing
|
|
34
|
+
* is known about the notebook and no link is reported as pending.
|
|
35
|
+
*/
|
|
36
|
+
export interface DiagnoseOptions extends Pick<NoteHost, 'resolve' | 'reaches' | 'keeps'> {
|
|
37
|
+
/** Whether to read each `mermaid` block with Mermaid, which is loaded for it. Defaults to true. */
|
|
38
|
+
readonly diagrams?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** The table of §7.13, as `spec.json` carries it: a test holds the two equal. */
|
|
41
|
+
export declare const NOTICES: Readonly<Record<NoticeCode, {
|
|
42
|
+
readonly severity: NoticeSeverity;
|
|
43
|
+
readonly section: string;
|
|
44
|
+
readonly message: string;
|
|
45
|
+
}>>;
|
|
46
|
+
/**
|
|
47
|
+
* The notices of a note, in the order of the note. `markdown` is the note as a host keeps it,
|
|
48
|
+
* frontmatter included; the positions are into it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function diagnose(markdown: string, options?: DiagnoseOptions): Promise<Notice[]>;
|
package/dist/diagnose.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The notices of a note (§7.13): where what is drawn will not be what was most likely typed.
|
|
3
|
+
*
|
|
4
|
+
* There is no invalid note (§6.4), so nothing here accepts or refuses: it reads a note the way the
|
|
5
|
+
* reading surface reads it — the same frontmatter cut, its comments taken out (`hideComments`), the
|
|
6
|
+
* same parser and the same remark plugins (`reading.ts`) — and reports, by a stable code, the places where the page will differ from the
|
|
7
|
+
* text, or where the note lacks something the specification reserves a place for. A notice never
|
|
8
|
+
* blocks a save and never changes a byte.
|
|
9
|
+
*
|
|
10
|
+
* It runs with no DOM and no React, so a host can call it on a server, from a tool that writes a
|
|
11
|
+
* note, or beside an editor. KaTeX checks a formula as `rehype-katex` would typeset it, and Mermaid
|
|
12
|
+
* reads a diagram as the page would draw it, loaded only when a note carries one.
|
|
13
|
+
*/
|
|
14
|
+
import katex from 'katex';
|
|
15
|
+
import remarkParse from 'remark-parse';
|
|
16
|
+
import { unified } from 'unified';
|
|
17
|
+
import { bodyStart, codeRegions, hideComments, insideCode, toUnixNewlines } from './markdown.js';
|
|
18
|
+
import { readProperties } from './properties.js';
|
|
19
|
+
import { readingPlugins } from './reading.js';
|
|
20
|
+
/** The table of §7.13, as `spec.json` carries it: a test holds the two equal. */
|
|
21
|
+
export const NOTICES = {
|
|
22
|
+
'fence-unclosed': { severity: 'notice', section: '3.7', message: 'This fence is never closed: it runs to the end of its block, and everything after it is code.' },
|
|
23
|
+
'math-unclosed': { severity: 'notice', section: '7.8', message: 'This $$ block is never closed: it runs to the end of its block, and everything after it is a formula.' },
|
|
24
|
+
'formula-invalid': { severity: 'notice', section: '7.8', message: 'This formula cannot be typeset, and is shown as its source.' },
|
|
25
|
+
'diagram-invalid': { severity: 'notice', section: '7.2', message: 'This diagram cannot be read, and is shown as its source or its error.' },
|
|
26
|
+
'frontmatter-unclosed': { severity: 'notice', section: '6.1', message: 'This frontmatter is never closed by a ---: the note has no properties, and its lines are body text.' },
|
|
27
|
+
'name-missing': { severity: 'notice', section: '6.5', message: 'The note has no name: a link reaches it only through an alias.' },
|
|
28
|
+
'name-unusable': { severity: 'notice', section: '6.5', message: 'This name is not a single text value, so the note is unnamed.' },
|
|
29
|
+
'reserved-not-a-list': { severity: 'notice', section: '6.4', message: 'This value is not a list, so it is an ordinary attribute.' },
|
|
30
|
+
'aliases-missing': { severity: 'hint', section: '6.4', message: 'The note has no aliases: only its name catches a link.' },
|
|
31
|
+
'tags-missing': { severity: 'hint', section: '6.4', message: 'The note has no tags: it has no subjects to be filtered by.' },
|
|
32
|
+
'heading-first-level': { severity: 'notice', section: '3.3', message: 'The first heading of the body is not level one.' },
|
|
33
|
+
'heading-skipped-level': { severity: 'notice', section: '3.3', message: 'This heading skips a level below the heading before it.' },
|
|
34
|
+
'footnote-undefined': { severity: 'notice', section: '7.12', message: 'This footnote has no definition, and is drawn as its characters.' },
|
|
35
|
+
'footnote-unreferenced': { severity: 'notice', section: '7.12', message: 'Nothing references this footnote, so it is not drawn.' },
|
|
36
|
+
'raw-html': { severity: 'notice', section: '7.9', message: 'Raw HTML is drawn as text, tags and all.' },
|
|
37
|
+
'strikethrough-single-tilde': { severity: 'notice', section: '4.3', message: 'One tilde strikes nothing: the tildes stay on the page. Strikethrough is ~~two~~.' },
|
|
38
|
+
'link-pending': { severity: 'hint', section: '7.4', message: 'No note answers this link yet, so it is drawn as pending.' },
|
|
39
|
+
'embed-pending': { severity: 'hint', section: '7.3', message: 'No note and no kept file answers this embed, so it is drawn as a pending link.' },
|
|
40
|
+
};
|
|
41
|
+
/** A footnote reference left as text, which is one with no definition (§7.12). */
|
|
42
|
+
const UNDEFINED_REFERENCE = /\[\^[^\]\s]+\]/g;
|
|
43
|
+
/** A run between single tildes, which GFM as read here leaves as characters (§4.3). */
|
|
44
|
+
const SINGLE_TILDE = /(?<![~\\])~(?!~)(?=\S)[^~\n]*?\S~(?!~)/g;
|
|
45
|
+
/** A wikilink, with the bang of an embed: the shape `resolveWikilinks` reads. */
|
|
46
|
+
const WIKILINK = /(!)?\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/g;
|
|
47
|
+
/** A closing fence or `$$`, once the markers of its container are taken off. */
|
|
48
|
+
const CONTAINER_PREFIX = /^[ \t>]*/;
|
|
49
|
+
const KEY = /^([A-Za-z0-9_-]+)\s*:/;
|
|
50
|
+
/**
|
|
51
|
+
* The notices of a note, in the order of the note. `markdown` is the note as a host keeps it,
|
|
52
|
+
* frontmatter included; the positions are into it.
|
|
53
|
+
*/
|
|
54
|
+
export async function diagnose(markdown, options = {}) {
|
|
55
|
+
const text = toUnixNewlines(markdown);
|
|
56
|
+
const lineStarts = [0];
|
|
57
|
+
for (let index = 0; index < text.length; index++)
|
|
58
|
+
if (text[index] === '\n')
|
|
59
|
+
lineStarts.push(index + 1);
|
|
60
|
+
const pointOf = (offset) => {
|
|
61
|
+
let low = 0;
|
|
62
|
+
let high = lineStarts.length - 1;
|
|
63
|
+
while (low < high) {
|
|
64
|
+
const middle = (low + high + 1) >> 1;
|
|
65
|
+
if ((lineStarts[middle] ?? 0) <= offset)
|
|
66
|
+
low = middle;
|
|
67
|
+
else
|
|
68
|
+
high = middle - 1;
|
|
69
|
+
}
|
|
70
|
+
return { line: low + 1, column: offset - (lineStarts[low] ?? 0) + 1 };
|
|
71
|
+
};
|
|
72
|
+
const found = [];
|
|
73
|
+
const report = (code, start, end = start, detail) => {
|
|
74
|
+
const from = pointOf(start);
|
|
75
|
+
const to = pointOf(end);
|
|
76
|
+
const { severity, section, message } = NOTICES[code];
|
|
77
|
+
found.push({
|
|
78
|
+
at: start,
|
|
79
|
+
notice: {
|
|
80
|
+
code,
|
|
81
|
+
severity,
|
|
82
|
+
section,
|
|
83
|
+
message: detail ? `${message} (${detail})` : message,
|
|
84
|
+
line: from.line,
|
|
85
|
+
column: from.column,
|
|
86
|
+
endLine: to.line,
|
|
87
|
+
endColumn: to.column,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const split = bodyStart(text);
|
|
92
|
+
frontmatter(text, split, report);
|
|
93
|
+
// A comment is never drawn, so nothing it holds raises a notice; positions are mapped back over it.
|
|
94
|
+
const hidden = hideComments(text.slice(split));
|
|
95
|
+
const body = hidden.text;
|
|
96
|
+
const processor = unified().use(remarkParse).use(readingPlugins(() => undefined));
|
|
97
|
+
const parsed = processor.parse(body);
|
|
98
|
+
// The text as the parser found it, before a plugin cut a run of it and dropped its position.
|
|
99
|
+
const read = structuredClone(parsed);
|
|
100
|
+
const tree = processor.runSync(parsed, body);
|
|
101
|
+
const span = (node) => {
|
|
102
|
+
const start = node.position?.start.offset;
|
|
103
|
+
const end = node.position?.end.offset;
|
|
104
|
+
return start === undefined || end === undefined ? null : [split + hidden.original(start), split + hidden.original(end, true)];
|
|
105
|
+
};
|
|
106
|
+
const diagrams = [];
|
|
107
|
+
const referenced = new Set();
|
|
108
|
+
const definitions = [];
|
|
109
|
+
walk(tree, (node) => {
|
|
110
|
+
const at = span(node);
|
|
111
|
+
if (!at)
|
|
112
|
+
return;
|
|
113
|
+
const source = text.slice(at[0], at[1]);
|
|
114
|
+
if (node.type === 'code' && /^\s*(`{3,}|~{3,})/.test(source)) {
|
|
115
|
+
if (!closes(source, /^(`{3,}|~{3,})/))
|
|
116
|
+
report('fence-unclosed', at[0], at[1]);
|
|
117
|
+
else if (node.lang === 'mermaid')
|
|
118
|
+
diagrams.push({ source: node.value ?? '', at });
|
|
119
|
+
}
|
|
120
|
+
else if (node.type === 'math') {
|
|
121
|
+
if (!closes(source, /^(\${2,})/))
|
|
122
|
+
report('math-unclosed', at[0], at[1]);
|
|
123
|
+
else if (!typesets(node.value ?? '', true))
|
|
124
|
+
report('formula-invalid', at[0], at[1]);
|
|
125
|
+
}
|
|
126
|
+
else if (node.type === 'inlineMath') {
|
|
127
|
+
if (!typesets(node.value ?? '', false))
|
|
128
|
+
report('formula-invalid', at[0], at[1]);
|
|
129
|
+
}
|
|
130
|
+
else if (node.type === 'html') {
|
|
131
|
+
report('raw-html', at[0], at[1]);
|
|
132
|
+
}
|
|
133
|
+
else if (node.type === 'footnoteReference') {
|
|
134
|
+
referenced.add(String(node.identifier ?? '').toUpperCase());
|
|
135
|
+
}
|
|
136
|
+
else if (node.type === 'footnoteDefinition') {
|
|
137
|
+
definitions.push(node);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
for (const definition of definitions) {
|
|
141
|
+
const at = span(definition);
|
|
142
|
+
if (at && !referenced.has(String(definition.identifier ?? '').toUpperCase())) {
|
|
143
|
+
report('footnote-unreferenced', at[0], at[1]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
outline(tree, span, report);
|
|
147
|
+
// What stays characters on the page is found in the text the parser read.
|
|
148
|
+
walk(read, (node) => {
|
|
149
|
+
if (node.type !== 'text')
|
|
150
|
+
return;
|
|
151
|
+
const start = node.position?.start.offset;
|
|
152
|
+
const end = node.position?.end.offset;
|
|
153
|
+
if (start === undefined || end === undefined)
|
|
154
|
+
return;
|
|
155
|
+
const source = body.slice(start, end);
|
|
156
|
+
for (const [pattern, code] of [
|
|
157
|
+
[UNDEFINED_REFERENCE, 'footnote-undefined'],
|
|
158
|
+
[SINGLE_TILDE, 'strikethrough-single-tilde'],
|
|
159
|
+
]) {
|
|
160
|
+
for (const match of source.matchAll(pattern)) {
|
|
161
|
+
const from = start + match.index;
|
|
162
|
+
report(code, split + hidden.original(from), split + hidden.original(from + match[0].length, true));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
pending(hidden, split, options, report);
|
|
167
|
+
if (diagrams.length > 0 && options.diagrams !== false) {
|
|
168
|
+
const mermaid = (await import('mermaid')).default;
|
|
169
|
+
for (const diagram of diagrams) {
|
|
170
|
+
try {
|
|
171
|
+
await mermaid.parse(diagram.source);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (unreadable(error))
|
|
175
|
+
report('diagram-invalid', diagram.at[0], diagram.at[1]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return found.sort((a, b) => a.at - b.at).map(({ notice }) => notice);
|
|
180
|
+
}
|
|
181
|
+
function walk(node, visit) {
|
|
182
|
+
visit(node);
|
|
183
|
+
for (const child of node.children ?? [])
|
|
184
|
+
walk(child, visit);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Whether a fenced block or a `$$` block was closed: its last line, once its container's markers
|
|
188
|
+
* are off, is a run of the opening character at least as long as the opening one, and nothing else.
|
|
189
|
+
*/
|
|
190
|
+
function closes(source, opening) {
|
|
191
|
+
const lines = source.split('\n');
|
|
192
|
+
const run = opening.exec((lines[0] ?? '').replace(CONTAINER_PREFIX, ''))?.[1] ?? '';
|
|
193
|
+
if (lines.length < 2 || run.length === 0)
|
|
194
|
+
return false;
|
|
195
|
+
const last = (lines[lines.length - 1] ?? '').replace(CONTAINER_PREFIX, '').trimEnd();
|
|
196
|
+
const closing = new RegExp(`^\\${run[0]}{${run.length},}$`);
|
|
197
|
+
return closing.test(last);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Whether Mermaid refused a diagram for what it says, rather than for where it runs. Its parsers
|
|
201
|
+
* answer with a parse error, and an unknown diagram type with its own error; with no DOM, reading a
|
|
202
|
+
* label of some diagrams fails on the sanitiser instead, and that failure is no notice: a diagram
|
|
203
|
+
* Mermaid could not finish reading here may well be drawn on the page.
|
|
204
|
+
*/
|
|
205
|
+
function unreadable(error) {
|
|
206
|
+
if (typeof error !== 'object' || error === null)
|
|
207
|
+
return false;
|
|
208
|
+
const failure = error;
|
|
209
|
+
return (typeof failure.hash === 'object' ||
|
|
210
|
+
failure.name === 'UnknownDiagramError' ||
|
|
211
|
+
failure.constructor?.name === 'MermaidParseError');
|
|
212
|
+
}
|
|
213
|
+
/** Whether KaTeX typesets a formula, as `rehype-katex` would. */
|
|
214
|
+
function typesets(formula, display) {
|
|
215
|
+
try {
|
|
216
|
+
katex.renderToString(formula, { displayMode: display, throwOnError: true, strict: 'ignore' });
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** The reserved keys of the frontmatter (§6.4, §6.5), and a block that never closes (§6.1). */
|
|
224
|
+
function frontmatter(text, split, report) {
|
|
225
|
+
const opened = text.startsWith('---');
|
|
226
|
+
if (opened && text.indexOf('\n---', 3) === -1)
|
|
227
|
+
report('frontmatter-unclosed', 0, text.indexOf('\n') === -1 ? 3 : text.indexOf('\n'));
|
|
228
|
+
// Where each key was written, to place a notice on it.
|
|
229
|
+
const keys = new Map();
|
|
230
|
+
if (opened && split > 0) {
|
|
231
|
+
let at = text.indexOf('\n') + 1;
|
|
232
|
+
for (const line of text.slice(at, split).split('\n')) {
|
|
233
|
+
const key = KEY.exec(line)?.[1];
|
|
234
|
+
if (key && !keys.has(key))
|
|
235
|
+
keys.set(key, at);
|
|
236
|
+
at += line.length + 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const lineEnd = (at) => {
|
|
240
|
+
const end = text.indexOf('\n', at);
|
|
241
|
+
return end === -1 ? text.length : end;
|
|
242
|
+
};
|
|
243
|
+
const properties = readProperties(text);
|
|
244
|
+
const name = keys.get('name');
|
|
245
|
+
if (name === undefined)
|
|
246
|
+
report('name-missing', 0);
|
|
247
|
+
else if (properties.name === null)
|
|
248
|
+
report('name-unusable', name, lineEnd(name));
|
|
249
|
+
for (const [key, code] of [
|
|
250
|
+
['aliases', 'aliases-missing'],
|
|
251
|
+
['tags', 'tags-missing'],
|
|
252
|
+
]) {
|
|
253
|
+
const at = keys.get(key);
|
|
254
|
+
const entry = properties.entries.find((candidate) => candidate.key === key);
|
|
255
|
+
if (at === undefined || !entry)
|
|
256
|
+
report(code, 0);
|
|
257
|
+
else if (entry.shape === 'empty')
|
|
258
|
+
report(code, at, lineEnd(at));
|
|
259
|
+
else if (entry.shape !== 'list')
|
|
260
|
+
report('reserved-not-a-list', at, lineEnd(at), key);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** The outline of the body (§3.3): the headings at its top level, outside code. */
|
|
264
|
+
function outline(tree, span, report) {
|
|
265
|
+
let previous = null;
|
|
266
|
+
for (const node of tree.children ?? []) {
|
|
267
|
+
if (node.type !== 'heading' || node.depth === undefined)
|
|
268
|
+
continue;
|
|
269
|
+
const at = span(node);
|
|
270
|
+
if (at) {
|
|
271
|
+
if (previous === null && node.depth > 1)
|
|
272
|
+
report('heading-first-level', at[0], at[1]);
|
|
273
|
+
else if (previous !== null && node.depth > previous + 1)
|
|
274
|
+
report('heading-skipped-level', at[0], at[1]);
|
|
275
|
+
}
|
|
276
|
+
previous = node.depth;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The wikilinks and embeds no note answers, when the host says which notes exist: a link reaches
|
|
281
|
+
* a note, and an embed a note or a kept file (§5.2, §5.8). Read on the body outside code, as
|
|
282
|
+
* `resolveWikilinks` reads it.
|
|
283
|
+
*/
|
|
284
|
+
function pending(hidden, split, options, report) {
|
|
285
|
+
const body = hidden.text;
|
|
286
|
+
const { resolve, reaches, keeps = () => false } = options;
|
|
287
|
+
if (!resolve && !reaches)
|
|
288
|
+
return;
|
|
289
|
+
const answered = (name) => reaches ? reaches(name.normalize('NFC')) > 0 : resolve(name.normalize('NFC')) !== null;
|
|
290
|
+
const code = codeRegions(body);
|
|
291
|
+
for (const match of body.matchAll(WIKILINK)) {
|
|
292
|
+
if (insideCode(code, match.index))
|
|
293
|
+
continue;
|
|
294
|
+
const name = (match[2] ?? '').split('#')[0]?.trim().replace(/\\$/, '').trim() ?? '';
|
|
295
|
+
// `[[#Section]]` is a link within the note itself.
|
|
296
|
+
if (!name)
|
|
297
|
+
continue;
|
|
298
|
+
const embed = match[1] !== undefined;
|
|
299
|
+
if (answered(name) || (embed && keeps(name)))
|
|
300
|
+
continue;
|
|
301
|
+
const end = match.index + match[0].length;
|
|
302
|
+
report(embed ? 'embed-pending' : 'link-pending', split + hidden.original(match.index), split + hidden.original(end, true), name);
|
|
303
|
+
}
|
|
304
|
+
}
|
package/dist/markdown.d.ts
CHANGED
|
@@ -41,6 +41,26 @@ export declare function insideCode(regions: Array<[number, number]>, at: number)
|
|
|
41
41
|
export declare function tableRegions(body: string): Array<[number, number]>;
|
|
42
42
|
/** Whether an offset falls inside one of them. */
|
|
43
43
|
export declare function insideTable(regions: Array<[number, number]>, at: number): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The comments of a text (§7.6), as half-open ranges: from a `%%` outside code to the next `%%`
|
|
46
|
+
* outside code, across inline markup and blank lines alike. A `%%` with no partner opens nothing.
|
|
47
|
+
*
|
|
48
|
+
* A comment that is all of its lines takes the line break after it too, so a comment written on
|
|
49
|
+
* lines of its own leaves no empty line behind to loosen a list or split a paragraph.
|
|
50
|
+
*
|
|
51
|
+
* MemorySmith's reading page hides a comment inside one run of text only, so one that holds a
|
|
52
|
+
* link, a tag, emphasis or a blank line reaches the page whole; this is found on the text, before
|
|
53
|
+
* anything is parsed, so nothing a comment holds is ever read.
|
|
54
|
+
*/
|
|
55
|
+
export declare function commentRegions(text: string): Array<[number, number]>;
|
|
56
|
+
/** A text with its comments taken out, and where an offset into it falls in the text it came from. */
|
|
57
|
+
export interface HiddenComments {
|
|
58
|
+
readonly text: string;
|
|
59
|
+
/** The offset in the original text of an offset into `text`; an `end` stays before a comment it meets. */
|
|
60
|
+
readonly original: (offset: number, end?: boolean) => number;
|
|
61
|
+
}
|
|
62
|
+
/** The text a reader is shown: its comments taken out (§7.6). The note itself is never changed. */
|
|
63
|
+
export declare function hideComments(text: string): HiddenComments;
|
|
44
64
|
/** Runs a rewrite over everything except code, which is copied through. */
|
|
45
65
|
export declare function outsideCode(body: string, rewrite: (text: string) => string): string;
|
|
46
66
|
export declare function resolveWikilinks(body: string, resolve: (name: string) => string | null,
|
package/dist/markdown.js
CHANGED
|
@@ -254,6 +254,54 @@ export function tableRegions(body) {
|
|
|
254
254
|
export function insideTable(regions, at) {
|
|
255
255
|
return regions.some(([start, end]) => at >= start && at < end);
|
|
256
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* The comments of a text (§7.6), as half-open ranges: from a `%%` outside code to the next `%%`
|
|
259
|
+
* outside code, across inline markup and blank lines alike. A `%%` with no partner opens nothing.
|
|
260
|
+
*
|
|
261
|
+
* A comment that is all of its lines takes the line break after it too, so a comment written on
|
|
262
|
+
* lines of its own leaves no empty line behind to loosen a list or split a paragraph.
|
|
263
|
+
*
|
|
264
|
+
* MemorySmith's reading page hides a comment inside one run of text only, so one that holds a
|
|
265
|
+
* link, a tag, emphasis or a blank line reaches the page whole; this is found on the text, before
|
|
266
|
+
* anything is parsed, so nothing a comment holds is ever read.
|
|
267
|
+
*/
|
|
268
|
+
export function commentRegions(text) {
|
|
269
|
+
const code = codeRegions(text);
|
|
270
|
+
const markers = [...text.matchAll(/%%/g)].map((match) => match.index).filter((at) => !insideCode(code, at));
|
|
271
|
+
const regions = [];
|
|
272
|
+
for (let index = 0; index + 1 < markers.length; index += 2) {
|
|
273
|
+
let start = markers[index];
|
|
274
|
+
let end = markers[index + 1] + 2;
|
|
275
|
+
const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
|
276
|
+
const lineEnd = text.indexOf('\n', end) === -1 ? text.length : text.indexOf('\n', end);
|
|
277
|
+
if (/^[ \t]*$/.test(text.slice(lineStart, start)) && /^[ \t]*$/.test(text.slice(end, lineEnd))) {
|
|
278
|
+
start = lineStart;
|
|
279
|
+
end = lineEnd === text.length ? lineEnd : lineEnd + 1;
|
|
280
|
+
}
|
|
281
|
+
regions.push([start, end]);
|
|
282
|
+
}
|
|
283
|
+
return regions;
|
|
284
|
+
}
|
|
285
|
+
/** The text a reader is shown: its comments taken out (§7.6). The note itself is never changed. */
|
|
286
|
+
export function hideComments(text) {
|
|
287
|
+
const regions = commentRegions(text);
|
|
288
|
+
if (regions.length === 0)
|
|
289
|
+
return { text, original: (offset) => offset };
|
|
290
|
+
let shown = '';
|
|
291
|
+
let cursor = 0;
|
|
292
|
+
// Where each comment was, in the shown text, and how much of the original it took.
|
|
293
|
+
const cuts = [];
|
|
294
|
+
for (const [start, end] of regions) {
|
|
295
|
+
shown += text.slice(cursor, start);
|
|
296
|
+
cuts.push({ at: shown.length, length: end - start });
|
|
297
|
+
cursor = end;
|
|
298
|
+
}
|
|
299
|
+
shown += text.slice(cursor);
|
|
300
|
+
return {
|
|
301
|
+
text: shown,
|
|
302
|
+
original: (offset, end = false) => offset + cuts.filter((cut) => (end ? cut.at < offset : cut.at <= offset)).reduce((total, cut) => total + cut.length, 0),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
257
305
|
/** Runs a rewrite over everything except code, which is copied through. */
|
|
258
306
|
export function outsideCode(body, rewrite) {
|
|
259
307
|
let out = '';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PluggableList } from 'unified';
|
|
2
|
+
/**
|
|
3
|
+
* The remark plugins of the reading surface, in their order. `planFootnotes` reads a note
|
|
4
|
+
* through the same list, so it counts the references this surface draws and no other.
|
|
5
|
+
*/
|
|
6
|
+
export declare function readingPlugins(titleOf: (kind: string) => string | undefined): PluggableList;
|
package/dist/reading.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The remark plugins of the reading surface, apart from React: the drawing, the footnote plan
|
|
2
|
+
// and the diagnostic read a note through the same list, so a notice never disagrees with the page.
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import remarkMath from 'remark-math';
|
|
5
|
+
import { remarkCallouts } from './remark-callouts.js';
|
|
6
|
+
import { remarkBlockIds, remarkHighlight, remarkMathDollarRule } from './remark-ring.js';
|
|
7
|
+
/**
|
|
8
|
+
* The remark plugins of the reading surface, in their order. `planFootnotes` reads a note
|
|
9
|
+
* through the same list, so it counts the references this surface draws and no other.
|
|
10
|
+
*/
|
|
11
|
+
export function readingPlugins(titleOf) {
|
|
12
|
+
return [
|
|
13
|
+
/**
|
|
14
|
+
* `singleTilde: false` is a decision and not a default.
|
|
15
|
+
*
|
|
16
|
+
* GFM specifies strikethrough as `~~x~~`; GitHub also accepts a
|
|
17
|
+
* single tilde, outside its own specification. Here that extension
|
|
18
|
+
* actively misrenders a notation the specification leaves out:
|
|
19
|
+
* somebody writing `H~2~O` for a subscript would get `H` struck-out
|
|
20
|
+
* `2` `O`, which is a worse answer than nothing. With it off the
|
|
21
|
+
* characters stay on the page, and the author can see they got what
|
|
22
|
+
* the specification says they get, which is nothing.
|
|
23
|
+
*/
|
|
24
|
+
[remarkGfm, { singleTilde: false }],
|
|
25
|
+
[remarkCallouts, { titleOf }],
|
|
26
|
+
remarkHighlight,
|
|
27
|
+
remarkBlockIds,
|
|
28
|
+
remarkMath,
|
|
29
|
+
// After remark-math, and it gives back what was never a formula.
|
|
30
|
+
remarkMathDollarRule,
|
|
31
|
+
];
|
|
32
|
+
}
|
package/dist/remark-ring.d.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The rest of the MemorySmith notation, on the reading surface: marked text,
|
|
3
|
-
*
|
|
4
|
-
* mathematics (§7.8).
|
|
3
|
+
* block identifiers (§7.5, §7.7) and the dollar rule of mathematics (§7.8).
|
|
5
4
|
*
|
|
6
|
-
*
|
|
5
|
+
* They are remark plugins rather than passes over the string, for the
|
|
7
6
|
* reason `remark-callouts.ts` already gives: a `==` inside a code fence is not
|
|
8
7
|
* a highlight, and only the parser can tell the difference. Rewriting the text
|
|
9
8
|
* before it is parsed is how a notation starts firing inside examples.
|
|
10
9
|
*
|
|
10
|
+
* Comments (§7.6) are the one exception, and they are taken out before the
|
|
11
|
+
* parser by `hideComments` (`markdown.ts`), which finds code first: a comment
|
|
12
|
+
* may hold markup and blank lines, and once parsed it is spread over nodes no
|
|
13
|
+
* plugin on a run of text can see whole.
|
|
14
|
+
*
|
|
11
15
|
* None of them touches the bytes. The reading surface is a rendering of the
|
|
12
16
|
* note and never a rewriting of it, so what a host stores and returns is
|
|
13
|
-
* unaffected by everything here
|
|
14
|
-
* `%%comment%%` an asymmetry that has to be declared rather than a deletion.
|
|
17
|
+
* unaffected by everything here.
|
|
15
18
|
*/
|
|
16
19
|
interface MdastNode {
|
|
17
20
|
type: string;
|
|
@@ -23,7 +26,6 @@ interface MdastNode {
|
|
|
23
26
|
};
|
|
24
27
|
}
|
|
25
28
|
export declare function remarkHighlight(): (tree: MdastNode) => void;
|
|
26
|
-
export declare function remarkComments(): (tree: MdastNode) => void;
|
|
27
29
|
export declare function remarkBlockIds(): (tree: MdastNode) => void;
|
|
28
30
|
/**
|
|
29
31
|
* The `$` that is not math (§7.8).
|
package/dist/remark-ring.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The rest of the MemorySmith notation, on the reading surface: marked text,
|
|
3
|
-
*
|
|
4
|
-
* mathematics (§7.8).
|
|
3
|
+
* block identifiers (§7.5, §7.7) and the dollar rule of mathematics (§7.8).
|
|
5
4
|
*
|
|
6
|
-
*
|
|
5
|
+
* They are remark plugins rather than passes over the string, for the
|
|
7
6
|
* reason `remark-callouts.ts` already gives: a `==` inside a code fence is not
|
|
8
7
|
* a highlight, and only the parser can tell the difference. Rewriting the text
|
|
9
8
|
* before it is parsed is how a notation starts firing inside examples.
|
|
10
9
|
*
|
|
10
|
+
* Comments (§7.6) are the one exception, and they are taken out before the
|
|
11
|
+
* parser by `hideComments` (`markdown.ts`), which finds code first: a comment
|
|
12
|
+
* may hold markup and blank lines, and once parsed it is spread over nodes no
|
|
13
|
+
* plugin on a run of text can see whole.
|
|
14
|
+
*
|
|
11
15
|
* None of them touches the bytes. The reading surface is a rendering of the
|
|
12
16
|
* note and never a rewriting of it, so what a host stores and returns is
|
|
13
|
-
* unaffected by everything here
|
|
14
|
-
* `%%comment%%` an asymmetry that has to be declared rather than a deletion.
|
|
17
|
+
* unaffected by everything here.
|
|
15
18
|
*/
|
|
16
19
|
/** Walks every node, letting the visitor rewrite a parent's children. */
|
|
17
20
|
function walk(node, visit) {
|
|
@@ -45,29 +48,6 @@ export function remarkHighlight() {
|
|
|
45
48
|
});
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
|
-
/**
|
|
49
|
-
* `%%comment%%` leaves the page and stays in the file (§7.6).
|
|
50
|
-
*
|
|
51
|
-
* The asymmetry is the decision and it is declared, not discovered: the bytes
|
|
52
|
-
* are untouched, so whatever returns the note returns the comment, and an
|
|
53
|
-
* agent therefore sees what a person on the page does not. Text the
|
|
54
|
-
* author did not want *read on the page* is still text the author wrote, and
|
|
55
|
-
* deleting bytes to make a page tidier is not something this product does.
|
|
56
|
-
*/
|
|
57
|
-
const COMMENT = /%%([\s\S]*?)%%/g;
|
|
58
|
-
export function remarkComments() {
|
|
59
|
-
return (tree) => walk(tree, (parent) => {
|
|
60
|
-
parent.children = (parent.children ?? []).flatMap((child) => {
|
|
61
|
-
if (!isText(child) || !child.value?.includes('%%'))
|
|
62
|
-
return [child];
|
|
63
|
-
const stripped = child.value.replace(COMMENT, '');
|
|
64
|
-
// A text that was nothing but a comment leaves no empty text node
|
|
65
|
-
// behind. The paragraph that held it stays, empty, as it does on
|
|
66
|
-
// MemorySmith's reading page.
|
|
67
|
-
return stripped.length === 0 ? [] : [{ ...child, value: stripped }];
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
51
|
/**
|
|
72
52
|
* `^identifier` at the end of a block names that block (§7.7), and is never
|
|
73
53
|
* rendered as text. What it names is resolved by the embed, not here: this
|
package/dist/tasklist.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* by design, because it flattens lists for display, and rebuilding the
|
|
12
12
|
* document from it would hand the notebook a file nobody typed.
|
|
13
13
|
*/
|
|
14
|
-
import { fenceRegions } from './markdown.js';
|
|
14
|
+
import { commentRegions, fenceRegions } from './markdown.js';
|
|
15
15
|
/**
|
|
16
16
|
* A task item, as GFM defines it, at the start of a list item — after any
|
|
17
17
|
* block quote markers in front of it, because a callout is a block quote and a
|
|
@@ -25,7 +25,7 @@ import { fenceRegions } from './markdown.js';
|
|
|
25
25
|
*/
|
|
26
26
|
const TASK = /^(?:[ \t]*>[ \t]?)*[ \t]*(?:[-*+]|\d+[.)])[ \t]+\[([ xX])\]/gm;
|
|
27
27
|
/**
|
|
28
|
-
* Blanks out fenced blocks while keeping every offset intact. A task item
|
|
28
|
+
* Blanks out fenced blocks and comments while keeping every offset intact. A task item
|
|
29
29
|
* inside a fence is an example, not a box, and counting it would shift the
|
|
30
30
|
* index and toggle a different item: the worst possible defect here, because
|
|
31
31
|
* it is silent and lands in the wrong place.
|
|
@@ -37,7 +37,11 @@ const TASK = /^(?:[ \t]*>[ \t]?)*[ \t]*(?:[-*+]|\d+[.)])[ \t]+\[([ xX])\]/gm;
|
|
|
37
37
|
function maskFences(text) {
|
|
38
38
|
let masked = '';
|
|
39
39
|
let cursor = 0;
|
|
40
|
-
|
|
40
|
+
// A comment is never drawn (§7.6), so a box inside one is no box, on either side of the count.
|
|
41
|
+
const hidden = [...fenceRegions(text), ...commentRegions(text)].sort((a, b) => a[0] - b[0]);
|
|
42
|
+
for (const [start, end] of hidden) {
|
|
43
|
+
if (start < cursor)
|
|
44
|
+
continue;
|
|
41
45
|
masked += text.slice(cursor, start) + text.slice(start, end).replace(/[^\n]/g, ' ');
|
|
42
46
|
cursor = end;
|
|
43
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memorysmithapp/markdown-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A React component that draws a MemorySmith note exactly as MemorySmith's reading page does.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
"types": "./dist/index.d.ts",
|
|
43
43
|
"default": "./dist/index.js"
|
|
44
44
|
},
|
|
45
|
+
"./diagnose": {
|
|
46
|
+
"types": "./dist/diagnose.d.ts",
|
|
47
|
+
"default": "./dist/diagnose.js"
|
|
48
|
+
},
|
|
45
49
|
"./styles.css": "./styles.css",
|
|
46
50
|
"./structure.css": "./structure.css",
|
|
47
51
|
"./theme-default.css": "./theme-default.css"
|
|
@@ -63,7 +67,7 @@
|
|
|
63
67
|
"unified": "11.0.5"
|
|
64
68
|
},
|
|
65
69
|
"devDependencies": {
|
|
66
|
-
"@memorysmithapp/markdown-spec": "0.
|
|
70
|
+
"@memorysmithapp/markdown-spec": "0.3.0",
|
|
67
71
|
"@types/hast": "3.0.5",
|
|
68
72
|
"@types/react": "19.2.18",
|
|
69
73
|
"@types/react-dom": "19.2.4",
|