@godxjp/markdown 31.27.0 → 31.28.1

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.
@@ -10,7 +10,7 @@
10
10
  * and the kit's own:
11
11
  * callout{kind,title} · details · detailsSummary · detailsContent · columns · column{width} ·
12
12
  * embed{target} (block `![[x]]`) · wikilink{target,heading,label} · embedInline{target} ·
13
- * rawBlock{source} · rawInline{source}
13
+ * rawBlock{source} · rawInline{source} · frontmatter{source} (YAML, first block only)
14
14
  * Marks: bold · italic · strike · code · link{href,title}.
15
15
  */
16
16
  type Mark = {
@@ -43,6 +43,20 @@ declare const COLUMNS_MAX = 4;
43
43
  /** Markdown → document (plain JSON, ProseMirror/Tiptap shape). Pure: no DOM. */
44
44
  declare function parse(markdown: string): Doc;
45
45
 
46
+ type OutlineEntry = {
47
+ depth: number;
48
+ text: string;
49
+ id: string;
50
+ };
51
+ /**
52
+ * The body's headings with the ids `<Markdown>` gives them — GitHub slugs, de-duplicated in
53
+ * document order, the renderer's default rule — for a table of contents (the kit's `Anchor`). Pure.
54
+ * A host that passes its own `headingId` resolver to `<Markdown>` must use that same resolver here.
55
+ */
56
+ declare function outline(markdown: string): OutlineEntry[];
57
+ /** The body's YAML front matter, verbatim (without the `---` fences), or null. Parse it with your YAML library. */
58
+ declare function frontmatterOf(markdown: string): string | null;
59
+
46
60
  /** Document → canonical Markdown. Pure: no DOM. */
47
61
  declare function serialize(doc: Doc | DocNode): string;
48
62
 
@@ -54,4 +68,4 @@ declare const CODEC_ID = "kit-md@1";
54
68
  /** `serialize(parse(markdown))` — the canonical form of a body, exactly what the editor saves. */
55
69
  declare function normalize(markdown: string): string;
56
70
 
57
- export { CALLOUT_KINDS, CODEC_ID, COLUMNS_MAX, COLUMNS_MIN, type CalloutKind, type Doc, type DocNode, type Mark, normalize, parse, serialize };
71
+ export { CALLOUT_KINDS, CODEC_ID, COLUMNS_MAX, COLUMNS_MIN, type CalloutKind, type Doc, type DocNode, type Mark, type OutlineEntry, frontmatterOf, normalize, outline, parse, serialize };
@@ -73,15 +73,17 @@ function readColumns(node, nested) {
73
73
  import { directive } from "micromark-extension-directive";
74
74
  import { gfm } from "micromark-extension-gfm";
75
75
  import { cjkFriendlyExtension } from "micromark-extension-cjk-friendly";
76
+ import { frontmatter } from "micromark-extension-frontmatter";
77
+ import { frontmatterFromMarkdown, frontmatterToMarkdown } from "mdast-util-frontmatter";
76
78
  import { directiveFromMarkdown } from "mdast-util-directive";
77
79
  import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
78
80
  function micromarkExtensions() {
79
81
  const [container] = directive().flow[58];
80
82
  const containerOnly = { flow: { 58: container } };
81
- return [gfm(), cjkFriendlyExtension(), containerOnly];
83
+ return [gfm(), cjkFriendlyExtension(), frontmatter(["yaml"]), containerOnly];
82
84
  }
83
85
  function fromMarkdownExtensions() {
84
- return [gfmFromMarkdown(), directiveFromMarkdown()];
86
+ return [gfmFromMarkdown(), directiveFromMarkdown(), frontmatterFromMarkdown(["yaml"])];
85
87
  }
86
88
  var WIDE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{1F300}-\u{1F64F}\u{1F900}-\u{1F9FF}\u{20000}-\u{3FFFD}]/u;
87
89
  function displayWidth(value) {
@@ -92,6 +94,9 @@ function displayWidth(value) {
92
94
  function toMarkdownGfm() {
93
95
  return gfmToMarkdown({ stringLength: displayWidth });
94
96
  }
97
+ function toMarkdownFrontmatter() {
98
+ return frontmatterToMarkdown(["yaml"]);
99
+ }
95
100
 
96
101
  // src/codec/options.ts
97
102
  function containerDirective(node, _, state, info) {
@@ -130,6 +135,7 @@ function toMarkdownOptions() {
130
135
  resourceLink: false,
131
136
  extensions: [
132
137
  toMarkdownGfm(),
138
+ toMarkdownFrontmatter(),
133
139
  { handlers: { containerDirective } }
134
140
  ]
135
141
  };
@@ -345,6 +351,8 @@ function block(node, depth) {
345
351
  }
346
352
  case "containerDirective":
347
353
  return [directiveBlock(node, depth)];
354
+ case "yaml":
355
+ return [{ type: "frontmatter", attrs: { source: node.value } }];
348
356
  default:
349
357
  return [raw(node, false)];
350
358
  }
@@ -536,6 +544,8 @@ function block2(node) {
536
544
  return [{ type: "html", value: `![[${String(attrs.target)}]]` }];
537
545
  case "rawBlock":
538
546
  return [{ type: "html", value: String(attrs.source) }];
547
+ case "frontmatter":
548
+ return [{ type: "yaml", value: String(attrs.source) }];
539
549
  default:
540
550
  return [];
541
551
  }
@@ -560,6 +570,29 @@ function serialize(doc) {
560
570
  return decodeAttentionReferences(tidy);
561
571
  }
562
572
 
573
+ // src/codec/outline.ts
574
+ import GithubSlugger from "github-slugger";
575
+ import { visit } from "unist-util-visit";
576
+ function textOf(node) {
577
+ if (node.type === "text" || node.type === "inlineCode" || node.type === "html") return node.value;
578
+ if (node.type === "image" || node.type === "imageReference") return "";
579
+ if ("children" in node) return node.children.map(textOf).join("");
580
+ return "";
581
+ }
582
+ function outline(markdown) {
583
+ const slugger = new GithubSlugger();
584
+ const entries = [];
585
+ visit(parseMdast(markdown.replace(/\r\n?/g, "\n")), "heading", (node) => {
586
+ const text2 = textOf(node);
587
+ entries.push({ depth: node.depth, text: text2, id: slugger.slug(text2) });
588
+ });
589
+ return entries;
590
+ }
591
+ function frontmatterOf(markdown) {
592
+ const first = parseMdast(markdown.replace(/\r\n?/g, "\n")).children[0];
593
+ return first?.type === "yaml" ? first.value : null;
594
+ }
595
+
563
596
  // src/codec/index.ts
564
597
  var CODEC_ID = "kit-md@1";
565
598
  function normalize(markdown) {
@@ -570,7 +603,9 @@ export {
570
603
  CODEC_ID,
571
604
  COLUMNS_MAX,
572
605
  COLUMNS_MIN,
606
+ frontmatterOf,
573
607
  normalize,
608
+ outline,
574
609
  parse,
575
610
  serialize
576
611
  };
package/dist/index.d.ts CHANGED
@@ -133,6 +133,19 @@ type MarkdownProps = {
133
133
  * default — this package carries no i18n; a host passes its localized strings.
134
134
  */
135
135
  calloutTitles?: Partial<Record<CalloutKind, string>>;
136
+ /**
137
+ * Where an EXTERNAL link opens. An external link is an absolute http(s) URL whose origin differs
138
+ * from `origin`. `"new-tab"` gives it `target="_blank"` and `rel="noopener noreferrer"`. Relative
139
+ * links, `#anchors`, wikilinks resolved to a path, and `mailto:` stay in the tab. The default,
140
+ * `"same-tab"`, changes nothing. Markdown has no syntax for `target`, so this is a renderer policy.
141
+ */
142
+ externalLinks?: "same-tab" | "new-tab";
143
+ /**
144
+ * The page origin that `externalLinks` compares against. Defaults to `location.origin`. Pass it
145
+ * when rendering on the server, or every absolute link counts as external there and hydration
146
+ * disagrees on same-origin ones.
147
+ */
148
+ origin?: string;
136
149
  };
137
150
  /**
138
151
  * THE ONE RENDERER (gh#1108). GFM — tables, task lists, fenced code, autolinks, footnotes,
@@ -144,7 +157,7 @@ type MarkdownProps = {
144
157
  * Renders bare elements; the host wraps it (`<Prose>` from `@godxjp/ui/data-display`), so this
145
158
  * package carries no UI-kit dependency.
146
159
  */
147
- declare function Markdown({ children, remarkPlugins, rehypePlugins, schema, components, resolveUrl, headingId, mermaid, allowedElements, disallowedElements, allowElement, unwrapDisallowed, shortCellLength, calloutTitles, }: MarkdownProps): React.JSX.Element;
160
+ declare function Markdown({ children, remarkPlugins, rehypePlugins, schema, components, resolveUrl, headingId, mermaid, allowedElements, disallowedElements, allowElement, unwrapDisallowed, shortCellLength, calloutTitles, externalLinks, origin, }: MarkdownProps): React.JSX.Element;
148
161
 
149
162
  type MermaidSvgCheck = {
150
163
  ok: true;
package/dist/index.js CHANGED
@@ -339,15 +339,17 @@ function columnShares(columns) {
339
339
  import { directive } from "micromark-extension-directive";
340
340
  import { gfm } from "micromark-extension-gfm";
341
341
  import { cjkFriendlyExtension } from "micromark-extension-cjk-friendly";
342
+ import { frontmatter } from "micromark-extension-frontmatter";
343
+ import { frontmatterFromMarkdown, frontmatterToMarkdown } from "mdast-util-frontmatter";
342
344
  import { directiveFromMarkdown } from "mdast-util-directive";
343
345
  import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
344
346
  function micromarkExtensions() {
345
347
  const [container] = directive().flow[58];
346
348
  const containerOnly = { flow: { 58: container } };
347
- return [gfm(), cjkFriendlyExtension(), containerOnly];
349
+ return [gfm(), cjkFriendlyExtension(), frontmatter(["yaml"]), containerOnly];
348
350
  }
349
351
  function fromMarkdownExtensions() {
350
- return [gfmFromMarkdown(), directiveFromMarkdown()];
352
+ return [gfmFromMarkdown(), directiveFromMarkdown(), frontmatterFromMarkdown(["yaml"])];
351
353
  }
352
354
 
353
355
  // src/directives.ts
@@ -367,7 +369,7 @@ function remarkKitSyntax(options = {}) {
367
369
  return function() {
368
370
  const data = this.data();
369
371
  (data.micromarkExtensions ??= []).push(...micromarkExtensions().slice(1));
370
- (data.fromMarkdownExtensions ??= []).push(fromMarkdownExtensions()[1]);
372
+ (data.fromMarkdownExtensions ??= []).push(...fromMarkdownExtensions().slice(1));
371
373
  const walk = (node, inColumn) => {
372
374
  if (node.type === "blockquote") callout(node, titles);
373
375
  else if (node.type === "containerDirective") directive2(node, inColumn);
@@ -375,7 +377,10 @@ function remarkKitSyntax(options = {}) {
375
377
  const isColumn = node.type === "containerDirective" && node.name === "column";
376
378
  for (const child of node.children) walk(child, inColumn || isColumn);
377
379
  };
378
- return (tree) => walk(tree, false);
380
+ return (tree) => {
381
+ tree.children = tree.children.filter((node) => node.type !== "yaml");
382
+ walk(tree, false);
383
+ };
379
384
  };
380
385
  }
381
386
  function callout(node, titles) {
@@ -470,6 +475,25 @@ function rehypeShortCells(options) {
470
475
  });
471
476
  };
472
477
  }
478
+ function rehypeExternalLinks(options) {
479
+ return (tree) => {
480
+ if (options.policy !== "new-tab") return;
481
+ const origin = options.origin ?? globalThis.location?.origin;
482
+ visit2(tree, "element", (node) => {
483
+ if (node.tagName !== "a") return;
484
+ const href = node.properties?.href;
485
+ if (typeof href !== "string" || !/^https?:\/\//i.test(href)) return;
486
+ let target;
487
+ try {
488
+ target = new URL(href);
489
+ } catch {
490
+ return;
491
+ }
492
+ if (origin && target.origin === origin) return;
493
+ node.properties = { ...node.properties, target: "_blank", rel: ["noopener", "noreferrer"] };
494
+ });
495
+ };
496
+ }
473
497
  function omitNode(props) {
474
498
  const { node: _node, ...rest } = props;
475
499
  return rest;
@@ -496,7 +520,9 @@ function Markdown({
496
520
  allowElement,
497
521
  unwrapDisallowed,
498
522
  shortCellLength = 24,
499
- calloutTitles
523
+ calloutTitles,
524
+ externalLinks = "same-tab",
525
+ origin
500
526
  }) {
501
527
  const sanitizeSchema = React3.useMemo(() => extendSchema(schema), [schema]);
502
528
  const kitSyntax = React3.useMemo(() => remarkKitSyntax({ calloutTitles }), [calloutTitles]);
@@ -524,6 +550,7 @@ function Markdown({
524
550
  [rehypeHeadingIds, { resolve: headingId }],
525
551
  [rehypeSanitize, sanitizeSchema],
526
552
  [rehypeShortCells, { max: shortCellLength }],
553
+ [rehypeExternalLinks, { policy: externalLinks, origin }],
527
554
  ...rehypePlugins
528
555
  ],
529
556
  urlTransform: safeUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godxjp/markdown",
3
- "version": "31.27.0",
3
+ "version": "31.28.1",
4
4
  "description": "The one Markdown renderer for GoDX apps: GFM + heading anchors, one sanitiser schema, a shared fixture corpus, and Mermaid drawn only after its SVG passes a fail-closed gate.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,10 +36,12 @@
36
36
  "github-slugger": "^2.0.0",
37
37
  "mdast-util-directive": "^3.1.1",
38
38
  "mdast-util-from-markdown": "^2.1.0",
39
+ "mdast-util-frontmatter": "^2.0.1",
39
40
  "mdast-util-gfm": "^3.1.0",
40
41
  "mdast-util-to-markdown": "^2.2.0",
41
42
  "micromark-extension-cjk-friendly": "^2.0.2",
42
43
  "micromark-extension-directive": "^4.0.0",
44
+ "micromark-extension-frontmatter": "^2.0.0",
43
45
  "micromark-extension-gfm": "^3.0.0",
44
46
  "micromark-util-types": "^2.0.2",
45
47
  "react-markdown": "^10.1.0",