@memorysmithapp/markdown-renderer 0.1.2 → 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.
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The frontmatter of a note, read for drawing (§6).
3
+ *
4
+ * The frontmatter is content: what the author wrote about the note. The
5
+ * renderer reads it the way the specification reads it and hands it to the
6
+ * theme, which decides how — and whether — it is drawn. The default theme
7
+ * draws `name` as the note's title and every other entry as a property, by
8
+ * the shape of its value.
9
+ *
10
+ * It reads the subset of YAML §6.2 names — scalars, inline lists and dash
11
+ * lists, with one layer of matching quotes stripped — and nothing else; a line
12
+ * it does not read is left out, never guessed at. A value written as a
13
+ * wikilink, `"[[Note]]"`, is kept as written: the theme draws it as a link,
14
+ * resolved by the host as a link of the body is. That is a decision of the
15
+ * reading surface; it adds no edge, and what an Indexer reads is unchanged.
16
+ */
17
+ /** Above this a single value is prose rather than a category (§6.3). */
18
+ const CATEGORY_CEILING = 40;
19
+ const KEY = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/;
20
+ const DASH = /^\s+-\s+(.*)$/;
21
+ const DATE = /^(\d{4})-(\d{2})-(\d{2})/;
22
+ const BOOLEAN = /^(true|false|yes|no)$/i;
23
+ /** A value that is one wikilink and nothing else, which YAML would otherwise read as a nested list. */
24
+ const WIKILINK_VALUE = /^\[\[[^\]]+\]\]$/;
25
+ function unquote(value) {
26
+ const trimmed = value.trim();
27
+ const quoted = /^(["'])([\s\S]*)\1$/.exec(trimmed);
28
+ return (quoted ? (quoted[2] ?? '') : trimmed).trim();
29
+ }
30
+ /** The items of an inline list, split on the commas outside quotes and outside a wikilink. */
31
+ function inlineItems(inside) {
32
+ const items = [];
33
+ let current = '';
34
+ let quote = null;
35
+ let depth = 0;
36
+ for (let i = 0; i < inside.length; i++) {
37
+ const character = inside[i];
38
+ if (quote) {
39
+ if (character === quote)
40
+ quote = null;
41
+ }
42
+ else if (character === '"' || character === "'") {
43
+ quote = character;
44
+ }
45
+ else if (inside.startsWith('[[', i)) {
46
+ depth += 1;
47
+ current += '[[';
48
+ i += 1;
49
+ continue;
50
+ }
51
+ else if (inside.startsWith(']]', i) && depth > 0) {
52
+ depth -= 1;
53
+ current += ']]';
54
+ i += 1;
55
+ continue;
56
+ }
57
+ else if (character === ',' && depth === 0) {
58
+ items.push(current);
59
+ current = '';
60
+ continue;
61
+ }
62
+ current += character;
63
+ }
64
+ items.push(current);
65
+ return items.map(unquote).filter((item) => item.length > 0);
66
+ }
67
+ function isDate(value) {
68
+ const match = DATE.exec(value);
69
+ if (!match)
70
+ return false;
71
+ const [, year, month, day] = match.map(Number);
72
+ const date = new Date(Date.UTC(year, month - 1, day));
73
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
74
+ }
75
+ function scalarEntry(key, raw) {
76
+ const value = unquote(raw);
77
+ if (value.length === 0)
78
+ return { key, shape: 'empty', values: [] };
79
+ if (isDate(value))
80
+ return { key, shape: 'date', values: [value] };
81
+ if (BOOLEAN.test(value))
82
+ return { key, shape: 'boolean', values: [value] };
83
+ return { key, shape: value.length > CATEGORY_CEILING ? 'prose' : 'text', values: [value] };
84
+ }
85
+ /**
86
+ * The frontmatter of a note, or no name and no entries when it has none. The block begins on the
87
+ * first line with `---` and ends at the next line that is `---` (§6.1).
88
+ */
89
+ export function readProperties(markdown) {
90
+ const text = markdown.replace(/\r\n?/g, '\n');
91
+ const none = { name: null, entries: [] };
92
+ if (!text.startsWith('---\n'))
93
+ return none;
94
+ const end = text.indexOf('\n---', 3);
95
+ if (end === -1)
96
+ return none;
97
+ const lines = text.slice(4, end).split('\n');
98
+ const entries = [];
99
+ for (const line of lines) {
100
+ const pair = KEY.exec(line);
101
+ if (pair) {
102
+ const raw = (pair[2] ?? '').trim();
103
+ const inline = raw.startsWith('[') && raw.endsWith(']') && !WIKILINK_VALUE.test(raw);
104
+ entries.push({ key: pair[1], raw, items: inline ? inlineItems(raw.slice(1, -1)) : null });
105
+ continue;
106
+ }
107
+ const dash = DASH.exec(line);
108
+ const last = entries[entries.length - 1];
109
+ // A dash list belongs to the key above it, which was written with no value.
110
+ if (dash && last && (last.items !== null || last.raw === '')) {
111
+ const item = unquote(dash[1] ?? '');
112
+ if (item.length > 0)
113
+ last.items = [...(last.items ?? []), item];
114
+ }
115
+ }
116
+ let name = null;
117
+ const read = [];
118
+ for (const { key, raw, items } of entries) {
119
+ if (key === 'name') {
120
+ // Only a single text value names a note; any other shape leaves it unnamed (§6.5).
121
+ const value = items === null ? unquote(raw) : '';
122
+ name = value.length > 0 ? value : null;
123
+ continue;
124
+ }
125
+ // A list written with no item, `[]`, is an empty value, never a list (§6.3).
126
+ read.push(items === null
127
+ ? scalarEntry(key, raw)
128
+ : items.length === 0
129
+ ? { key, shape: 'empty', values: [] }
130
+ : { key, shape: 'list', values: items });
131
+ }
132
+ return { name, entries: read };
133
+ }
134
+ /**
135
+ * What a value may carry that is a link: a wikilink, a Markdown link to the web, or a bare web
136
+ * address — as MemorySmith's properties read it, and nothing else of Markdown.
137
+ */
138
+ export const VALUE_LINK = /\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]|\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|(https?:\/\/[^\s,]+)/g;
139
+ /** Whether a boolean value reads true: `true` or `yes`, in any case. */
140
+ export function isTrue(value) {
141
+ return /^(true|yes)$/i.test(value.trim());
142
+ }
143
+ /** The reserved keys a theme draws as properties (§6.4): `name` is the title, not a property. */
144
+ export const DRAWN_RESERVED_KEYS = ['aliases', 'tags'];
@@ -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;
@@ -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
+ }
@@ -19,7 +19,7 @@
19
19
  * can tell the difference.
20
20
  */
21
21
  /** `[!type]`, an optional fold marker, and whatever titles it. */
22
- const MARKER = /^\[!([A-Za-z][\w-]*)\][+-]?[ \t]*(.*)$/;
22
+ const MARKER = /^\[!([A-Za-z][\w-]*)\]([+-]?)[ \t]*(.*)$/;
23
23
  export function remarkCallouts(options = {}) {
24
24
  return (tree) => descend(tree, options);
25
25
  }
@@ -42,10 +42,12 @@ function rewrite(quote, options) {
42
42
  if (!marker)
43
43
  return;
44
44
  const kind = (marker[1] ?? '').toLowerCase();
45
- const { title, body } = split(marker[2] ?? '', broke === -1 ? null : lead.value.slice(broke + 1), inline);
45
+ const { title, body } = split(marker[3] ?? '', broke === -1 ? null : lead.value.slice(broke + 1), inline);
46
46
  quote.data = {
47
47
  hName: 'div',
48
- hProperties: { className: ['callout'], 'data-callout': kind },
48
+ // The fold marker travels too, for a theme that folds; the default theme
49
+ // draws a callout open whatever it says, as MemorySmith's page does.
50
+ hProperties: { className: ['callout'], 'data-callout': kind, ...(marker[2] ? { 'data-fold': marker[2] } : {}) },
49
51
  };
50
52
  quote.children = [
51
53
  {
@@ -1,17 +1,20 @@
1
1
  /**
2
2
  * The rest of the MemorySmith notation, on the reading surface: marked text,
3
- * comments, block identifiers (§7.5, §7.6, §7.7) and the dollar rule of
4
- * mathematics (§7.8).
3
+ * block identifiers (§7.5, §7.7) and the dollar rule of mathematics (§7.8).
5
4
  *
6
- * All three are remark plugins rather than passes over the string, for the
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 — which is the whole of what makes
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).
@@ -1,17 +1,20 @@
1
1
  /**
2
2
  * The rest of the MemorySmith notation, on the reading surface: marked text,
3
- * comments, block identifiers (§7.5, §7.6, §7.7) and the dollar rule of
4
- * mathematics (§7.8).
3
+ * block identifiers (§7.5, §7.7) and the dollar rule of mathematics (§7.8).
5
4
  *
6
- * All three are remark plugins rather than passes over the string, for the
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 — which is the whole of what makes
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
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The slots of a theme, where the reading surface hands a drawn notation over.
3
+ *
4
+ * Each component here draws exactly what the default theme draws when the
5
+ * theme leaves its slot empty, so a note with no theme is the same DOM it was
6
+ * before slots existed, and calls the slot with the notation already
7
+ * interpreted when the theme fills it. Nothing a slot is given lets it reach
8
+ * the source: what it draws is decided before it is called.
9
+ */
10
+ import { type HTMLAttributes, type ReactElement, type ReactNode } from 'react';
11
+ import type { ThemeComponents } from './theme.js';
12
+ /** The slot a theme fills, or `undefined`. */
13
+ export declare function useSlot<K extends keyof ThemeComponents>(name: K): ThemeComponents[K] | undefined;
14
+ /** Draws a slot as a component, so a slot may use hooks. */
15
+ export declare function drawSlot<P extends object>(slot: (props: P) => ReactNode, props: P): ReactNode;
16
+ type WithNode<T> = T & {
17
+ node?: unknown;
18
+ };
19
+ /** Whether a code element sits in a fenced block, which is what tells a block from a span. */
20
+ export declare const InBlock: import("react").Context<boolean>;
21
+ /** A `div` of the tree: a callout or its title, which `remarkCallouts` marks, or any other. */
22
+ export declare function MarkdownDiv({ className, children, node: _node, ...rest }: WithNode<HTMLAttributes<HTMLDivElement>>): import("react").JSX.Element;
23
+ export declare function MarkdownTable({ children, node, ...rest }: WithNode<HTMLAttributes<HTMLTableElement>>): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
24
+ export declare const headings: {
25
+ h1: (props: HTMLAttributes<HTMLHeadingElement>) => import("react").JSX.Element;
26
+ h2: (props: HTMLAttributes<HTMLHeadingElement>) => import("react").JSX.Element;
27
+ h3: (props: HTMLAttributes<HTMLHeadingElement>) => import("react").JSX.Element;
28
+ h4: (props: HTMLAttributes<HTMLHeadingElement>) => import("react").JSX.Element;
29
+ h5: (props: HTMLAttributes<HTMLHeadingElement>) => import("react").JSX.Element;
30
+ h6: (props: HTMLAttributes<HTMLHeadingElement>) => import("react").JSX.Element;
31
+ };
32
+ export declare function MarkdownBlockquote({ children, node: _node, ...rest }: WithNode<HTMLAttributes<HTMLQuoteElement>>): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
33
+ export declare function MarkdownSection({ children, node: _node, ...rest }: WithNode<HTMLAttributes<HTMLElement>>): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
34
+ /** Whether a code element is being drawn inside a fenced block. */
35
+ export declare function useInBlock(): boolean;
36
+ export {};
package/dist/slots.js ADDED
@@ -0,0 +1,124 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The slots of a theme, where the reading surface hands a drawn notation over.
4
+ *
5
+ * Each component here draws exactly what the default theme draws when the
6
+ * theme leaves its slot empty, so a note with no theme is the same DOM it was
7
+ * before slots existed, and calls the slot with the notation already
8
+ * interpreted when the theme fills it. Nothing a slot is given lets it reach
9
+ * the source: what it draws is decided before it is called.
10
+ */
11
+ import { Children, createContext, createElement, Fragment, isValidElement, useContext, } from 'react';
12
+ import { useHost } from './host.js';
13
+ /** The slot a theme fills, or `undefined`. */
14
+ export function useSlot(name) {
15
+ return useHost().theme.components?.[name];
16
+ }
17
+ /** Draws a slot as a component, so a slot may use hooks. */
18
+ export function drawSlot(slot, props) {
19
+ return createElement(slot, props);
20
+ }
21
+ /** Whether a code element sits in a fenced block, which is what tells a block from a span. */
22
+ export const InBlock = createContext(false);
23
+ // ── Callouts ─────────────────────────────────────────────────────────────────
24
+ /** The title `remarkCallouts` built, carried to the callout that holds it. */
25
+ function CalloutTitle({ children }) {
26
+ return _jsx("div", { className: "callout-title", children: children });
27
+ }
28
+ function Callout({ kind, fold, children }) {
29
+ const { theme } = useHost();
30
+ const slot = useSlot('callout');
31
+ const iconSlot = useSlot('calloutIcon');
32
+ const icon = theme.icons && Object.hasOwn(theme.icons, kind)
33
+ ? theme.icons[kind]
34
+ : iconSlot
35
+ ? drawSlot(iconSlot, { kind })
36
+ : null;
37
+ const parts = Children.toArray(children);
38
+ // The title arrives as the `div` react-markdown draws through `MarkdownDiv`.
39
+ const at = parts.findIndex((part) => isValidElement(part) &&
40
+ part.type === MarkdownDiv &&
41
+ (part.props.className?.split(/\s+/) ?? []).includes('callout-title'));
42
+ const title = at === -1 ? null : parts[at].props.children;
43
+ const titled = icon === null ? (_jsx(CalloutTitle, { children: title })) : (
44
+ // An icon of the theme's takes the place of the one the stylesheet draws.
45
+ _jsxs("div", { className: "callout-title has-icon", children: [_jsx("span", { className: "callout-icon", "aria-hidden": "true", children: icon }), title] }));
46
+ if (slot) {
47
+ const body = parts.filter((_, index) => index !== at);
48
+ return drawSlot(slot, { kind, fold, title, icon, children: body });
49
+ }
50
+ return (_jsx("div", { className: "callout", "data-callout": kind, ...(fold ? { 'data-fold': fold } : {}), children: parts.map((part, index) => (index === at ? _jsx(Fragment, { children: titled }, "title") : part)) }));
51
+ }
52
+ /** A `div` of the tree: a callout or its title, which `remarkCallouts` marks, or any other. */
53
+ export function MarkdownDiv({ className, children, node: _node, ...rest }) {
54
+ const classes = className?.split(/\s+/) ?? [];
55
+ const data = rest;
56
+ if (classes.includes('callout-title'))
57
+ return _jsx(CalloutTitle, { children: children });
58
+ if (classes.includes('callout')) {
59
+ const fold = data['data-fold'];
60
+ return (_jsx(Callout, { kind: String(data['data-callout'] ?? ''), fold: fold === '+' || fold === '-' ? fold : null, children: children }));
61
+ }
62
+ return (_jsx("div", { className: className, ...rest, children: children }));
63
+ }
64
+ /** The alignment of each column, read from the first row's cells. */
65
+ function alignOf(node) {
66
+ const rows = [];
67
+ const walk = (element) => {
68
+ if (element.tagName === 'tr')
69
+ rows.push(element);
70
+ else
71
+ for (const child of element.children ?? [])
72
+ walk(child);
73
+ };
74
+ if (node)
75
+ walk(node);
76
+ const first = rows[0];
77
+ return (first?.children ?? [])
78
+ .filter((cell) => cell.tagName === 'th' || cell.tagName === 'td')
79
+ .map((cell) => {
80
+ const align = cell.properties?.['align'];
81
+ return align === 'left' || align === 'center' || align === 'right' ? align : null;
82
+ });
83
+ }
84
+ export function MarkdownTable({ children, node, ...rest }) {
85
+ const slot = useSlot('table');
86
+ if (slot)
87
+ return drawSlot(slot, { align: alignOf(node), children });
88
+ return _jsx("table", { ...rest, children: children });
89
+ }
90
+ function Heading({ level, className, children, node: _node, ...rest }) {
91
+ const slot = useSlot('heading');
92
+ // The heading of the footnotes is the footnotes slot's, not a heading of the note.
93
+ if (slot && !className?.split(/\s+/).includes('footnotes-label'))
94
+ return drawSlot(slot, { level, children });
95
+ return createElement(`h${level}`, { className, ...rest }, children);
96
+ }
97
+ export const headings = {
98
+ h1: (props) => _jsx(Heading, { level: 1, ...props }),
99
+ h2: (props) => _jsx(Heading, { level: 2, ...props }),
100
+ h3: (props) => _jsx(Heading, { level: 3, ...props }),
101
+ h4: (props) => _jsx(Heading, { level: 4, ...props }),
102
+ h5: (props) => _jsx(Heading, { level: 5, ...props }),
103
+ h6: (props) => _jsx(Heading, { level: 6, ...props }),
104
+ };
105
+ export function MarkdownBlockquote({ children, node: _node, ...rest }) {
106
+ const slot = useSlot('blockquote');
107
+ if (slot)
108
+ return drawSlot(slot, { children });
109
+ return _jsx("blockquote", { ...rest, children: children });
110
+ }
111
+ export function MarkdownSection({ children, node: _node, ...rest }) {
112
+ const slot = useSlot('footnotes');
113
+ const { strings } = useHost();
114
+ const footnotes = rest['data-footnotes'] !== undefined;
115
+ if (slot && footnotes) {
116
+ const notes = Children.toArray(children).filter((child) => !(isValidElement(child) && child.props.id === 'footnote-label'));
117
+ return drawSlot(slot, { label: strings.footnotes, children: notes });
118
+ }
119
+ return _jsx("section", { ...rest, children: children });
120
+ }
121
+ /** Whether a code element is being drawn inside a fenced block. */
122
+ export function useInBlock() {
123
+ return useContext(InBlock);
124
+ }
package/dist/strings.d.ts CHANGED
@@ -8,6 +8,13 @@
8
8
  export interface NoteStrings {
9
9
  /** The title of an untitled callout, by kind (`note`, `warning`, …). */
10
10
  readonly callouts: Readonly<Record<string, string>>;
11
+ /** The heading of the properties a note's frontmatter declares. */
12
+ readonly properties: string;
13
+ /** The locale a date of a property is written in, as `Intl` takes it: `en-US`, `pt-BR`. */
14
+ readonly locale: string;
15
+ /** How a boolean property reads, true and false. */
16
+ readonly yes: string;
17
+ readonly no: string;
11
18
  /** The heading of the footnotes gathered at the end of a note. */
12
19
  readonly footnotes: string;
13
20
  /** The label of the way back from a footnote to its reference, counted from 1. */
package/dist/strings.js CHANGED
@@ -31,6 +31,10 @@ const CALLOUTS = {
31
31
  /** The words MemorySmith shows in English, used for every word a host does not give. */
32
32
  export const ENGLISH = {
33
33
  callouts: CALLOUTS,
34
+ properties: 'Properties',
35
+ locale: 'en-US',
36
+ yes: 'Yes',
37
+ no: 'No',
34
38
  footnotes: 'Notes',
35
39
  footnoteBack: (number) => `Back to reference ${number}`,
36
40
  pendingLink: 'Pending link: the target note does not exist yet.',
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
- for (const [start, end] of fenceRegions(text)) {
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
  }