@memorysmithapp/markdown-renderer 0.3.0 → 0.3.2

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/Markdown.js CHANGED
@@ -142,7 +142,7 @@ function MarkdownCode({ className, children, node: _node, ...rest }) {
142
142
  const inBlock = useInBlock();
143
143
  const inlineSlot = useSlot('inlineCode');
144
144
  if (className?.includes('language-mermaid')) {
145
- return _jsx(MermaidDiagram, { code: String(children).trim(), scheme: scheme });
145
+ return _jsx(MermaidDiagram, { code: String(children ?? '').trim(), scheme: scheme });
146
146
  }
147
147
  if (!inBlock && inlineSlot)
148
148
  return drawSlot(inlineSlot, { children });
@@ -151,7 +151,8 @@ function MarkdownCode({ className, children, node: _node, ...rest }) {
151
151
  // The source is read and never rewritten: what is on the page is the
152
152
  // bytes, split into spans. React-markdown hands the children of a fence as
153
153
  // a single string ending in the newline before the closing fence.
154
- const code = String(children).replace(/\n$/, '');
154
+ // An empty fence has no children at all, and is no code rather than the word `undefined`.
155
+ const code = String(children ?? '').replace(/\n$/, '');
155
156
  return (_jsx("code", { className: className, ...rest, children: toJsxRuntime(highlightTree(code, language), { Fragment, jsx, jsxs }) }));
156
157
  }
157
158
  return (_jsx("code", { className: className, ...rest, children: children }));
package/dist/diagnose.js CHANGED
@@ -25,7 +25,7 @@ export const NOTICES = {
25
25
  'diagram-invalid': { severity: 'notice', section: '7.2', message: 'This diagram cannot be read, and is shown as its source or its error.' },
26
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
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.' },
28
+ 'name-unusable': { severity: 'notice', section: '6.5', message: 'This name is not a single text value, or carries one of #, [, ] and |, so the note is unnamed.' },
29
29
  'reserved-not-a-list': { severity: 'notice', section: '6.4', message: 'This value is not a list, so it is an ordinary attribute.' },
30
30
  'aliases-missing': { severity: 'hint', section: '6.4', message: 'The note has no aliases: only its name catches a link.' },
31
31
  'tags-missing': { severity: 'hint', section: '6.4', message: 'The note has no tags: it has no subjects to be filtered by.' },
@@ -29,7 +29,10 @@ export interface PropertyEntry {
29
29
  }
30
30
  /** The frontmatter, read. */
31
31
  export interface NoteProperties {
32
- /** The name of the note: `name:` when it is a single text value (§6.5), else `null`. */
32
+ /**
33
+ * The name of the note: `name:` when it is a single text value (§6.5) that carries none of
34
+ * `#`, `[`, `]` and `|` (§5.3), else `null`.
35
+ */
33
36
  readonly name: string | null;
34
37
  /** Every other entry, in the order written. */
35
38
  readonly entries: readonly PropertyEntry[];
@@ -14,6 +14,8 @@
14
14
  * resolved by the host as a link of the body is. That is a decision of the
15
15
  * reading surface; it adds no edge, and what an Indexer reads is unchanged.
16
16
  */
17
+ /** The four delimiters of the form that addresses a note: a name carrying one is no name (§5.3). */
18
+ const UNADDRESSABLE = /[#[\]|]/;
17
19
  /** Above this a single value is prose rather than a category (§6.3). */
18
20
  const CATEGORY_CEILING = 40;
19
21
  const KEY = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/;
@@ -117,9 +119,11 @@ export function readProperties(markdown) {
117
119
  const read = [];
118
120
  for (const { key, raw, items } of entries) {
119
121
  if (key === 'name') {
120
- // Only a single text value names a note; any other shape leaves it unnamed (§6.5).
122
+ // Only a single text value names a note; any other shape leaves it unnamed (§6.5), and so
123
+ // does one carrying a delimiter of the form that addresses it (§5.3). The subset has no
124
+ // comments (§6.2): `name: Incident #4711` is that whole value, and it carries a `#`.
121
125
  const value = items === null ? unquote(raw) : '';
122
- name = value.length > 0 ? value : null;
126
+ name = value.length > 0 && !UNADDRESSABLE.test(value) ? value : null;
123
127
  continue;
124
128
  }
125
129
  // A list written with no item, `[]`, is an empty value, never a list (§6.3).
@@ -18,8 +18,12 @@
18
18
  * reads: a `[!note]` inside a code fence is not a callout, and only the parser
19
19
  * can tell the difference.
20
20
  */
21
- /** `[!type]`, an optional fold marker, and whatever titles it. */
22
- const MARKER = /^\[!([A-Za-z][\w-]*)\]([+-]?)[ \t]*(.*)$/;
21
+ /**
22
+ * `[!type]`, an optional fold marker, and whatever titles it. The type is a word
23
+ * in any script (§7.1): it opens with a letter and goes on in letters, marks,
24
+ * digits, `_` and `-`, so `[!atenção]` is a type as much as `[!warning]` is.
25
+ */
26
+ const MARKER = /^\[!(\p{L}[\p{L}\p{M}\p{N}_-]*)\]([+-]?)[ \t]*(.*)$/u;
23
27
  export function remarkCallouts(options = {}) {
24
28
  return (tree) => descend(tree, options);
25
29
  }
@@ -41,7 +45,7 @@ function rewrite(quote, options) {
41
45
  const marker = MARKER.exec(broke === -1 ? lead.value : lead.value.slice(0, broke));
42
46
  if (!marker)
43
47
  return;
44
- const kind = (marker[1] ?? '').toLowerCase();
48
+ const kind = (marker[1] ?? '').normalize('NFC').toLowerCase();
45
49
  const { title, body } = split(marker[3] ?? '', broke === -1 ? null : lead.value.slice(broke + 1), inline);
46
50
  quote.data = {
47
51
  hName: 'div',
@@ -54,8 +54,15 @@ export function remarkHighlight() {
54
54
  * plugin only keeps the marker off the page.
55
55
  */
56
56
  const BLOCK_ID = /[ \t]*\^([A-Za-z0-9-]+)[ \t]*$/;
57
+ /**
58
+ * The nodes a block ends in. The last text of a link, an emphasis or any other
59
+ * inline is not the end of a block: `[[Note#^id]]` keeps its `^id` on the label.
60
+ */
61
+ const BLOCKS = new Set(['paragraph', 'heading', 'tableCell']);
57
62
  export function remarkBlockIds() {
58
63
  return (tree) => walk(tree, (parent) => {
64
+ if (!BLOCKS.has(parent.type))
65
+ return;
59
66
  const children = parent.children ?? [];
60
67
  const last = children[children.length - 1];
61
68
  if (!last || !isText(last) || !last.value?.includes('^'))
package/dist/slots.js CHANGED
@@ -21,9 +21,17 @@ export function drawSlot(slot, props) {
21
21
  /** Whether a code element sits in a fenced block, which is what tells a block from a span. */
22
22
  export const InBlock = createContext(false);
23
23
  // ── Callouts ─────────────────────────────────────────────────────────────────
24
+ /**
25
+ * The words of a title, in one element: the title is laid out as a row beside
26
+ * its icon, and every text node and every `<strong>` of it would otherwise be
27
+ * an item of that row of its own, drawn as columns. One run wraps as a sentence.
28
+ */
29
+ function CalloutTitleText({ children }) {
30
+ return _jsx("span", { className: "callout-title-text", children: children });
31
+ }
24
32
  /** The title `remarkCallouts` built, carried to the callout that holds it. */
25
33
  function CalloutTitle({ children }) {
26
- return _jsx("div", { className: "callout-title", children: children });
34
+ return (_jsx("div", { className: "callout-title", children: _jsx(CalloutTitleText, { children: children }) }));
27
35
  }
28
36
  function Callout({ kind, fold, children }) {
29
37
  const { theme } = useHost();
@@ -42,7 +50,7 @@ function Callout({ kind, fold, children }) {
42
50
  const title = at === -1 ? null : parts[at].props.children;
43
51
  const titled = icon === null ? (_jsx(CalloutTitle, { children: title })) : (
44
52
  // 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] }));
53
+ _jsxs("div", { className: "callout-title has-icon", children: [_jsx("span", { className: "callout-icon", "aria-hidden": "true", children: icon }), _jsx(CalloutTitleText, { children: title })] }));
46
54
  if (slot) {
47
55
  const body = parts.filter((_, index) => index !== at);
48
56
  return drawSlot(slot, { kind, fold, title, icon, children: body });
package/dist/theme.d.ts CHANGED
@@ -53,6 +53,8 @@ export interface ThemeProperties {
53
53
  readonly calloutStop?: string;
54
54
  readonly calloutAside?: string;
55
55
  readonly calloutQuote?: string;
56
+ readonly calloutTitleLinkDecoration?: string;
57
+ readonly calloutTitleLinkOffset?: string;
56
58
  readonly iconNote?: string;
57
59
  readonly iconInfo?: string;
58
60
  readonly iconSummary?: string;
package/dist/theme.js CHANGED
@@ -26,6 +26,7 @@ export const THEME_PROPERTIES = Object.keys({
26
26
  codeBg: 1, codeColor: 1, codeBorder: 1, inlineCodeBg: 1, inlineCodePadding: 1,
27
27
  tokenKeyword: 1, tokenString: 1, tokenNumber: 1, tokenComment: 1, tokenPunctuation: 1,
28
28
  calloutInfo: 1, calloutIdea: 1, calloutDone: 1, calloutWarn: 1, calloutStop: 1, calloutAside: 1, calloutQuote: 1,
29
+ calloutTitleLinkDecoration: 1, calloutTitleLinkOffset: 1,
29
30
  iconNote: 1, iconInfo: 1, iconSummary: 1, iconTip: 1, iconSuccess: 1, iconQuestion: 1, iconWarning: 1,
30
31
  iconFailure: 1, iconDanger: 1, iconExample: 1, iconQuote: 1,
31
32
  fontSans: 1, fontDisplay: 1, fontMono: 1, fontHeading: 1,
@@ -72,5 +72,8 @@ export declare function isBlockAnchor(anchor: string): boolean;
72
72
  * The section of a document, cut syntactically: from the heading whose slug
73
73
  * matches the anchor to the next heading of equal or higher level. No notebook
74
74
  * convention takes part in this: the cut is syntactic.
75
+ *
76
+ * A line inside a fence is code and never a heading, so the `# Step 1` comment
77
+ * of a shell block neither opens a section nor ends one.
75
78
  */
76
79
  export declare function sectionOf(markdown: string, anchor: string): string | null;
@@ -16,7 +16,7 @@
16
16
  * and never blocks (§4.1). Wherever expansion cannot happen the embed becomes
17
17
  * a link and is never dropped, which is what `demoteEmbeds` does.
18
18
  */
19
- import { codeRegions, headingKey, insideCode, insideTable, outsideCode, tableRegions } from './markdown.js';
19
+ import { codeRegions, fenceRegions, headingKey, insideCode, insideTable, outsideCode, tableRegions } from './markdown.js';
20
20
  /** Same shape the link extractor matches, plus the leading `!`. */
21
21
  const EMBED = /!\[\[([^\]|#]+?)(?:#([^\]|]+?))?(?:\|[^\]]*?)?\]\]/g;
22
22
  /**
@@ -136,13 +136,22 @@ function escapeRegExp(value) {
136
136
  * The section of a document, cut syntactically: from the heading whose slug
137
137
  * matches the anchor to the next heading of equal or higher level. No notebook
138
138
  * convention takes part in this: the cut is syntactic.
139
+ *
140
+ * A line inside a fence is code and never a heading, so the `# Step 1` comment
141
+ * of a shell block neither opens a section nor ends one.
139
142
  */
140
143
  export function sectionOf(markdown, anchor) {
141
144
  const wanted = headingKey(anchor);
142
145
  const lines = markdown.split('\n');
146
+ const fences = fenceRegions(markdown);
143
147
  let start = -1;
144
148
  let level = 0;
149
+ let at = 0;
145
150
  for (let index = 0; index < lines.length; index++) {
151
+ const offset = at;
152
+ at += (lines[index] ?? '').length + 1;
153
+ if (insideCode(fences, offset))
154
+ continue;
146
155
  const heading = HEADING.exec(lines[index] ?? '');
147
156
  if (!heading)
148
157
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memorysmithapp/markdown-renderer",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "A React component that draws a MemorySmith note exactly as MemorySmith's reading page does.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -67,7 +67,7 @@
67
67
  "unified": "11.0.5"
68
68
  },
69
69
  "devDependencies": {
70
- "@memorysmithapp/markdown-spec": "0.3.0",
70
+ "@memorysmithapp/markdown-spec": "0.3.2",
71
71
  "@types/hast": "3.0.5",
72
72
  "@types/react": "19.2.18",
73
73
  "@types/react-dom": "19.2.4",
package/structure.css CHANGED
@@ -37,6 +37,13 @@
37
37
  border: 0;
38
38
  }
39
39
 
40
+ /* The words of a callout's title are one item of its row, so a title that
41
+ carries emphasis wraps as one sentence beside the icon, never as columns. */
42
+ .markdown .callout-title-text {
43
+ flex: 1 1 auto;
44
+ min-width: 0;
45
+ }
46
+
40
47
  /* An icon a theme draws as an element takes the place of the one the
41
48
  stylesheet draws as a mask, at the size of the title it sits in. */
42
49
  .markdown .callout-title.has-icon::before {
package/theme-default.css CHANGED
@@ -61,6 +61,11 @@
61
61
  --ms-callout-stop: var(--markdown-callout-stop, 180, 35, 24);
62
62
  --ms-callout-aside: var(--markdown-callout-aside, 109, 40, 217);
63
63
  --ms-callout-quote: var(--markdown-callout-quote, 84, 93, 115);
64
+ /* A link in a callout title is underlined: the title is in the callout's
65
+ colour and weight, and the blue family's colour is the accent's, so a link
66
+ there would read as more of the title. */
67
+ --ms-callout-title-link-decoration: var(--markdown-callout-title-link-decoration, underline 1px);
68
+ --ms-callout-title-link-offset: var(--markdown-callout-title-link-offset, 0.15em);
64
69
  --ms-font-sans: var(--markdown-font-sans, 'Inter', 'Segoe UI', system-ui, sans-serif);
65
70
  --ms-font-display: var(--markdown-font-display, 'Space Grotesk', 'Segoe UI', system-ui, sans-serif);
66
71
  --ms-font-mono: var(--markdown-font-mono, ui-monospace, 'Cascadia Code', Consolas, monospace);
@@ -360,6 +365,10 @@
360
365
  /* Room for the button, so the first line is never under it. */
361
366
  padding-right: 6.5rem;
362
367
  }
368
+ /* An empty block keeps the height of one line, so its button fits inside it. */
369
+ .markdown .code-block > pre > code:empty::before {
370
+ content: '\200b';
371
+ }
363
372
  .markdown pre code {
364
373
  background: none;
365
374
  color: var(--ms-text);
@@ -579,6 +588,13 @@
579
588
  font-weight: 600;
580
589
  line-height: var(--ms-line-height-tight);
581
590
  }
591
+ /* A link in the title is marked by more than its colour, which may be the
592
+ title's own; a pending link keeps its dashed rule, and a footnote's number
593
+ its own look. */
594
+ .markdown .callout-title :is(a, .wikilink):not(.wikilink-pending, [data-footnote-ref]) {
595
+ text-decoration: var(--ms-callout-title-link-decoration);
596
+ text-underline-offset: var(--ms-callout-title-link-offset);
597
+ }
582
598
  .markdown .callout-title::before {
583
599
  content: '';
584
600
  flex: 0 0 auto;