@kevinpeckham/barkdown 0.1.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,106 @@
1
+ /**
2
+ * Post-processing pass for footnote definitions in serialized markdown.
3
+ *
4
+ * marked-footnote renders nothing for a definition that is never
5
+ * referenced outside the footnote section itself, so emitting such a
6
+ * definition would vanish one trip late. This pass canonicalizes the
7
+ * output by dropping unreferenced definitions up front. Pure
8
+ * string → string; no DOM involvement.
9
+ */
10
+ /** A footnote definition line: `[^label]: …` at column 0. */
11
+ const DEF_LINE = /^\[\^([A-Za-z0-9_-]+)\]:/;
12
+ /** A fenced-code fence line (up to 3 leading spaces, ``` or ~~~). */
13
+ const FENCE_LINE = /^ {0,3}(?:`{3,}|~{3,})/;
14
+ /**
15
+ * Drop footnote definitions that nothing references. Dropping a
16
+ * definition can orphan another (its content held the only ref) —
17
+ * iterate to a fixpoint. Bounded by the number of definitions.
18
+ */
19
+ export function dropUnreferencedFootnoteDefs(markdown) {
20
+ let current = markdown;
21
+ for (let pass = 0; pass < 50; pass++) {
22
+ if (!/^\[\^[A-Za-z0-9_-]+\]:/m.test(current))
23
+ return current;
24
+ const { text, dropped } = dropDefsOnce(current);
25
+ current = text;
26
+ if (!dropped)
27
+ return current;
28
+ }
29
+ return current;
30
+ }
31
+ /** One sweep: remove every currently-unreferenced definition. */
32
+ function dropDefsOnce(markdown) {
33
+ const lines = markdown.split("\n");
34
+ const inCode = codeLineMask(lines);
35
+ const kept = [];
36
+ let dropped = false;
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const line = lines[i] ?? "";
39
+ const match = inCode[i] ? null : line.match(DEF_LINE);
40
+ if (match && !isReferenced(lines, inCode, match[1] ?? "")) {
41
+ dropped = true;
42
+ i = skipDroppedDefinition(lines, i);
43
+ continue;
44
+ }
45
+ kept.push(line);
46
+ }
47
+ return { text: kept.join("\n"), dropped };
48
+ }
49
+ /**
50
+ * Fenced-code regions are opaque: def-looking lines inside them are not
51
+ * definitions and ref-looking text is not a reference. Returns a
52
+ * per-line mask; the fence lines themselves count as code context.
53
+ */
54
+ function codeLineMask(lines) {
55
+ const inCode = [];
56
+ let codeOpen = false;
57
+ for (const line of lines) {
58
+ if (FENCE_LINE.test(line)) {
59
+ inCode.push(true); // the fence line itself is code context
60
+ codeOpen = !codeOpen;
61
+ }
62
+ else {
63
+ inCode.push(codeOpen);
64
+ }
65
+ }
66
+ return inCode;
67
+ }
68
+ /**
69
+ * A "reference" is any unescaped [^label] occurrence (outside code) that
70
+ * is not itself the prefix of a definition line — marked-footnote counts
71
+ * refs inside definition content (including self-refs).
72
+ */
73
+ function isReferenced(lines, inCode, label) {
74
+ const needle = new RegExp(`(?<!\\\\)\\[\\^${label}\\](:?)`, "g");
75
+ for (let i = 0; i < lines.length; i++) {
76
+ if (inCode[i])
77
+ continue;
78
+ const line = lines[i] ?? "";
79
+ for (const m of line.matchAll(needle)) {
80
+ if (!(m[1] === ":" && m.index === 0 && DEF_LINE.test(line))) {
81
+ return true;
82
+ }
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+ /**
88
+ * Skip past a dropped definition's continuation lines (4-space-indented,
89
+ * possibly separated by blank lines), plus one following blank line so
90
+ * no double blank is left behind. Returns the last consumed index.
91
+ */
92
+ function skipDroppedDefinition(lines, index) {
93
+ let i = index;
94
+ while (i + 1 < lines.length) {
95
+ const next = lines[i + 1] ?? "";
96
+ if (/^ {4}/.test(next))
97
+ i++;
98
+ else if (next.trim() === "" && /^ {4}/.test(lines[i + 2] ?? "x"))
99
+ i++;
100
+ else
101
+ break;
102
+ }
103
+ if ((lines[i + 1] ?? "x").trim() === "")
104
+ i++;
105
+ return i;
106
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Serialize a DOM subtree back to markdown. Inverse of `toDom` — walks
3
+ * the HTML shape that `marked` produces plus the tags contenteditable
4
+ * editors inject, and emits GFM-flavored markdown.
5
+ *
6
+ * Design principles:
7
+ * - Closed subset. We only serialize elements we know about. Anything
8
+ * else is preserved as raw `outerHTML` so leaks surface early rather
9
+ * than silently disappearing.
10
+ * - Adapter-seamed. Element inputs are walked through the structural
11
+ * `HtmlElementLike` interface; string inputs are parsed via a
12
+ * `DomAdapter` (browser document by default).
13
+ * - Cursor-agnostic. Selection state is the caller's responsibility;
14
+ * this function just reads DOM → returns a string.
15
+ * - No side effects. Pure. Callers can invoke it debounced without
16
+ * worrying about mutation of the input.
17
+ */
18
+ import type { DomAdapter, HtmlElementLike } from "./adapter.js";
19
+ export interface ToMarkdownOptions {
20
+ /**
21
+ * DOM adapter used to parse string inputs. Defaults to the platform
22
+ * document; required in runtimes without one (Node, Bun).
23
+ */
24
+ adapter?: DomAdapter;
25
+ }
26
+ export declare function toMarkdown(input: HtmlElementLike | string, options?: ToMarkdownOptions): string;