@aglyn/aglyn-markdown-editor 1.0.0-beta.143

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.
Files changed (30) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +34 -0
  3. package/package.json +40 -0
  4. package/src/index.d.ts +43 -0
  5. package/src/index.js +42 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/link-target-autocomplete.component.d.ts +60 -0
  8. package/src/lib/link-target-autocomplete.component.js +235 -0
  9. package/src/lib/link-target-autocomplete.component.js.map +1 -0
  10. package/src/lib/markdown-editor-toolbar.component.d.ts +14 -0
  11. package/src/lib/markdown-editor-toolbar.component.js +254 -0
  12. package/src/lib/markdown-editor-toolbar.component.js.map +1 -0
  13. package/src/lib/markdown-field.component.d.ts +43 -0
  14. package/src/lib/markdown-field.component.js +144 -0
  15. package/src/lib/markdown-field.component.js.map +1 -0
  16. package/src/lib/markdown-html-paste.d.ts +38 -0
  17. package/src/lib/markdown-html-paste.js +312 -0
  18. package/src/lib/markdown-html-paste.js.map +1 -0
  19. package/src/lib/markdown-link-dialog.component.d.ts +39 -0
  20. package/src/lib/markdown-link-dialog.component.js +195 -0
  21. package/src/lib/markdown-link-dialog.component.js.map +1 -0
  22. package/src/lib/markdown-lite-view.component.d.ts +6 -0
  23. package/src/lib/markdown-lite-view.component.js +243 -0
  24. package/src/lib/markdown-lite-view.component.js.map +1 -0
  25. package/src/lib/markdown-source-command.d.ts +51 -0
  26. package/src/lib/markdown-source-command.js +56 -0
  27. package/src/lib/markdown-source-command.js.map +1 -0
  28. package/src/lib/markdown-visual-editor.component.d.ts +109 -0
  29. package/src/lib/markdown-visual-editor.component.js +2019 -0
  30. package/src/lib/markdown-visual-editor.component.js.map +1 -0
@@ -0,0 +1,312 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * HTML clipboard → markdown-lite conversion for the visual editor
18
+ * (AGL-596). Pure functions with no editor state: `htmlToRows` parses a
19
+ * `text/html` clipboard payload and maps it onto the editor's row model,
20
+ * keeping only what the markdown-lite dialect can express — bold, italic,
21
+ * links, h2/h3 headings, flat bullet and numbered lists (AGL-1320), images,
22
+ * blockquotes (AGL-1315), and (AGL-981) code blocks and tables, the two
23
+ * blocks a pasted README is made of. Everything else flattens
24
+ * to plain text; nested marks flatten to the OUTERMOST mark. Row keys are
25
+ * module-local placeholders — the editor re-keys converted rows with its
26
+ * own key sequence on ingest.
27
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
28
+ let htmlRowSeq = 0;
29
+ const nextHtmlRowKey = ()=>`html-row-${++htmlRowSeq}`;
30
+ const collapseWhitespace = (text)=>text.replace(/\s+/g, ' ');
31
+ /** Block-level tags that terminate an inline run at the row level. */ const BLOCK_TAGS = new Set([
32
+ 'article',
33
+ 'aside',
34
+ 'blockquote',
35
+ 'div',
36
+ 'figure',
37
+ 'footer',
38
+ 'h1',
39
+ 'h2',
40
+ 'h3',
41
+ 'h4',
42
+ 'h5',
43
+ 'h6',
44
+ 'header',
45
+ 'hr',
46
+ 'img',
47
+ 'li',
48
+ 'main',
49
+ 'nav',
50
+ 'ol',
51
+ 'p',
52
+ 'pre',
53
+ 'section',
54
+ 'table',
55
+ 'ul'
56
+ ]);
57
+ const BLOCK_SELECTOR = Array.from(BLOCK_TAGS).join(', ');
58
+ const isBlockElement = (node)=>node.nodeType === Node.ELEMENT_NODE && BLOCK_TAGS.has(node.tagName.toLowerCase());
59
+ /**
60
+ * An inline-tagged element hiding block content — e.g. Google Docs wraps
61
+ * the whole clipboard in `<b style="font-weight:normal">`. Treated as a
62
+ * wrapper to recurse through, never as one giant mark.
63
+ */ const isInlineWrapperOfBlocks = (node)=>node.nodeType === Node.ELEMENT_NODE && !isBlockElement(node) && node.querySelector(BLOCK_SELECTOR) != null;
64
+ /**
65
+ * Merges adjacent same-type runs (same href for links), re-collapses
66
+ * whitespace across the merge seams and trims the ends of the sequence.
67
+ */ function finishInlines(inlines) {
68
+ const merged = [];
69
+ for (const inline of inlines){
70
+ if (!inline.text) continue;
71
+ const last = merged[merged.length - 1];
72
+ if (last && last.type === inline.type && (last.type !== 'link' || last.href === inline.href)) {
73
+ last.text = collapseWhitespace(last.text + inline.text);
74
+ } else {
75
+ merged.push(_extends({}, inline));
76
+ }
77
+ }
78
+ const first = merged[0];
79
+ if (first) first.text = first.text.replace(/^\s+/, '');
80
+ const last = merged[merged.length - 1];
81
+ if (last) last.text = last.text.replace(/\s+$/, '');
82
+ return merged.filter((inline)=>inline.text.length > 0);
83
+ }
84
+ function inlinesFromNodes(nodes) {
85
+ const out = [];
86
+ const pushText = (text)=>{
87
+ if (text) out.push({
88
+ type: 'text',
89
+ text
90
+ });
91
+ };
92
+ /**
93
+ * Marked elements flatten to their whole text content (the dialect has
94
+ * no nested marks), with edge whitespace moved OUT of the mark so the
95
+ * separating spaces around `<strong>`/`<a>` survive serialization.
96
+ */ const pushMarked = (el, build)=>{
97
+ var _el_textContent;
98
+ const text = collapseWhitespace((_el_textContent = el.textContent) != null ? _el_textContent : '');
99
+ const core = text.trim();
100
+ if (text.startsWith(' ')) pushText(' ');
101
+ if (core) out.push(build(core));
102
+ if (core && text.endsWith(' ')) pushText(' ');
103
+ };
104
+ const visit = (node)=>{
105
+ if (node.nodeType === Node.TEXT_NODE) {
106
+ var _node_textContent;
107
+ pushText(collapseWhitespace((_node_textContent = node.textContent) != null ? _node_textContent : ''));
108
+ return;
109
+ }
110
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
111
+ const el = node;
112
+ const tag = el.tagName.toLowerCase();
113
+ if (tag === 'script' || tag === 'style') return;
114
+ if (tag === 'br') {
115
+ pushText(' ');
116
+ return;
117
+ }
118
+ if (tag === 'strong' || tag === 'b') {
119
+ pushMarked(el, (text)=>({
120
+ type: 'bold',
121
+ text
122
+ }));
123
+ return;
124
+ }
125
+ if (tag === 'em' || tag === 'i') {
126
+ pushMarked(el, (text)=>({
127
+ type: 'italic',
128
+ text
129
+ }));
130
+ return;
131
+ }
132
+ if (tag === 'a' && el.getAttribute('href')) {
133
+ var _el_getAttribute;
134
+ // The RAW attribute (not el.href) so site paths like /pricing
135
+ // survive instead of resolving against the parser's base URL.
136
+ const href = (_el_getAttribute = el.getAttribute('href')) != null ? _el_getAttribute : '';
137
+ pushMarked(el, (text)=>({
138
+ type: 'link',
139
+ text,
140
+ href
141
+ }));
142
+ return;
143
+ }
144
+ Array.from(el.childNodes).forEach(visit);
145
+ };
146
+ nodes.forEach(visit);
147
+ return finishInlines(out);
148
+ }
149
+ /** Converts an element's children to markdown-lite inlines. */ export function htmlToInlines(node) {
150
+ return inlinesFromNodes(Array.from(node.childNodes));
151
+ }
152
+ /**
153
+ * Converts a `text/html` clipboard payload to editor rows. Unknown
154
+ * wrappers recurse so they never swallow content; rows with no inlines
155
+ * and no image are dropped.
156
+ */ export function htmlToRows(html) {
157
+ const doc = new DOMParser().parseFromString(html, 'text/html');
158
+ const rows = [];
159
+ const pushTextRow = (kind, inlines, /** Seeds a numbered run from `<ol start>` (AGL-1320). */ start)=>{
160
+ if (inlines.length === 0) return;
161
+ rows.push(_extends({
162
+ key: nextHtmlRowKey(),
163
+ kind,
164
+ inlines
165
+ }, start == null ? {} : {
166
+ start
167
+ }));
168
+ };
169
+ const visitBlock = (el)=>{
170
+ const tag = el.tagName.toLowerCase();
171
+ if (tag === 'h1' || tag === 'h2') {
172
+ pushTextRow('heading2', htmlToInlines(el));
173
+ return;
174
+ }
175
+ if (tag === 'h3' || tag === 'h4' || tag === 'h5' || tag === 'h6') {
176
+ pushTextRow('heading3', htmlToInlines(el));
177
+ return;
178
+ }
179
+ if (tag === 'ul' || tag === 'ol') {
180
+ // An `<ol>` becomes numbered rows (AGL-1320), carrying `start` on the
181
+ // first item that actually lands — an empty leading `<li>` produces no
182
+ // row, and the run has to start counting from the one that does.
183
+ const ordered = tag === 'ol';
184
+ const startAttr = el.getAttribute('start');
185
+ const parsedStart = Number(startAttr);
186
+ let pending = !ordered ? undefined : startAttr && Number.isFinite(parsedStart) ? Math.max(0, Math.trunc(parsedStart)) : 1;
187
+ for (const child of Array.from(el.children)){
188
+ const childTag = child.tagName.toLowerCase();
189
+ if (childTag === 'li') {
190
+ const before = rows.length;
191
+ pushTextRow(ordered ? 'orderedItem' : 'listItem', htmlToInlines(child), pending);
192
+ if (rows.length > before) pending = undefined;
193
+ } else if (childTag === 'ul' || childTag === 'ol') visitBlock(child);
194
+ }
195
+ return;
196
+ }
197
+ if (tag === 'li') {
198
+ pushTextRow('listItem', htmlToInlines(el));
199
+ return;
200
+ }
201
+ // A GitHub README's two workhorse blocks (AGL-981). `<pre>` keeps its
202
+ // line breaks verbatim — the whole point of a snippet — and the
203
+ // language comes off the `language-x` class GitHub puts on the inner
204
+ // `<code>`.
205
+ if (tag === 'pre') {
206
+ var _el_textContent;
207
+ const text = ((_el_textContent = el.textContent) != null ? _el_textContent : '').replace(/^(?:[ \t]*\n)+/, '').replace(/\s+$/, '');
208
+ if (text.trim()) {
209
+ var _ref, _ref1;
210
+ const code = el.querySelector('code');
211
+ const lang = /(?:^|\s)(?:language|lang)-([\w+#.-]+)/.exec((_ref = code == null ? void 0 : code.getAttribute('class')) != null ? _ref : '');
212
+ rows.push({
213
+ key: nextHtmlRowKey(),
214
+ kind: 'code',
215
+ lang: (_ref1 = lang == null ? void 0 : lang[1]) != null ? _ref1 : '',
216
+ text
217
+ });
218
+ }
219
+ return;
220
+ }
221
+ if (tag === 'table') {
222
+ const cellsOf = (tr)=>Array.from(tr.children).filter((cell)=>/^t[hd]$/.test(cell.tagName.toLowerCase()));
223
+ const cellRows = Array.from(el.querySelectorAll('tr'))// A nested table's rows belong to IT, not to this one; its cells
224
+ // still flatten into the containing cell's inlines, since the
225
+ // dialect has no nesting.
226
+ .filter((tr)=>tr.closest('table') === el).map(cellsOf).filter((cells)=>cells.length > 0);
227
+ const [header, ...body] = cellRows;
228
+ if (header) {
229
+ rows.push({
230
+ key: nextHtmlRowKey(),
231
+ kind: 'table',
232
+ // GitHub writes a column's alignment onto every cell of it.
233
+ align: header.map((cell)=>{
234
+ var _cell_getAttribute;
235
+ var _cell_style;
236
+ const align = (_cell_getAttribute = cell.getAttribute('align')) != null ? _cell_getAttribute : (_cell_style = cell.style) == null ? void 0 : _cell_style.textAlign;
237
+ return align === 'center' || align === 'right' ? align : 'left';
238
+ }),
239
+ header: header.map((cell)=>htmlToInlines(cell)),
240
+ // Squared off to the header, the shape the parser guarantees.
241
+ rows: body.map((cells)=>header.map((_, index)=>{
242
+ const cell = cells[index];
243
+ return cell ? htmlToInlines(cell) : [];
244
+ }))
245
+ });
246
+ }
247
+ return;
248
+ }
249
+ if (tag === 'img') {
250
+ const src = el.getAttribute('src');
251
+ if (src) {
252
+ var _el_getAttribute;
253
+ rows.push({
254
+ key: nextHtmlRowKey(),
255
+ kind: 'image',
256
+ src,
257
+ alt: (_el_getAttribute = el.getAttribute('alt')) != null ? _el_getAttribute : ''
258
+ });
259
+ }
260
+ return;
261
+ }
262
+ // A quote keeps its quoteness (AGL-1315): each paragraph of the
263
+ // blockquote becomes its own quote row; a flat blockquote is one.
264
+ if (tag === 'blockquote') {
265
+ const paragraphs = Array.from(el.querySelectorAll('p'));
266
+ if (paragraphs.length) {
267
+ for (const p of paragraphs)pushTextRow('quote', htmlToInlines(p));
268
+ } else {
269
+ pushTextRow('quote', htmlToInlines(el));
270
+ }
271
+ return;
272
+ }
273
+ if (tag === 'p' || tag === 'div' || tag === 'section') {
274
+ // A container with block children is a wrapper — recurse so the
275
+ // inner structure survives; otherwise it is one paragraph.
276
+ const wrapsBlocks = Array.from(el.children).some(isBlockElement);
277
+ if (wrapsBlocks) visitChildren(el);
278
+ else pushTextRow('paragraph', htmlToInlines(el));
279
+ return;
280
+ }
281
+ // Unknown block-ish element (figure, table, article…): recurse.
282
+ visitChildren(el);
283
+ };
284
+ /**
285
+ * Walks mixed content: consecutive inline nodes (bare text, marks,
286
+ * spans) group into ONE paragraph row; block elements flush the run
287
+ * and map to their own rows.
288
+ */ const visitChildren = (parent)=>{
289
+ let pendingInline = [];
290
+ const flush = ()=>{
291
+ if (pendingInline.length === 0) return;
292
+ pushTextRow('paragraph', inlinesFromNodes(pendingInline));
293
+ pendingInline = [];
294
+ };
295
+ for (const child of Array.from(parent.childNodes)){
296
+ if (isBlockElement(child)) {
297
+ flush();
298
+ visitBlock(child);
299
+ } else if (isInlineWrapperOfBlocks(child)) {
300
+ flush();
301
+ visitChildren(child);
302
+ } else {
303
+ pendingInline.push(child);
304
+ }
305
+ }
306
+ flush();
307
+ };
308
+ visitChildren(doc.body);
309
+ return rows;
310
+ }
311
+
312
+ //# sourceMappingURL=markdown-html-paste.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../libs/aglyn-markdown-editor/src/lib/markdown-html-paste.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * HTML clipboard → markdown-lite conversion for the visual editor\n * (AGL-596). Pure functions with no editor state: `htmlToRows` parses a\n * `text/html` clipboard payload and maps it onto the editor's row model,\n * keeping only what the markdown-lite dialect can express — bold, italic,\n * links, h2/h3 headings, flat bullet and numbered lists (AGL-1320), images,\n * blockquotes (AGL-1315), and (AGL-981) code blocks and tables, the two\n * blocks a pasted README is made of. Everything else flattens\n * to plain text; nested marks flatten to the OUTERMOST mark. Row keys are\n * module-local placeholders — the editor re-keys converted rows with its\n * own key sequence on ingest.\n */\n\nimport type { MarkdownInline } from '@aglyn/aglyn'\nimport type { EditorRow } from './markdown-visual-editor.component'\n\nlet htmlRowSeq = 0\nconst nextHtmlRowKey = (): string => `html-row-${++htmlRowSeq}`\n\nconst collapseWhitespace = (text: string): string => text.replace(/\\s+/g, ' ')\n\n/** Block-level tags that terminate an inline run at the row level. */\nconst BLOCK_TAGS = new Set([\n 'article',\n 'aside',\n 'blockquote',\n 'div',\n 'figure',\n 'footer',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'header',\n 'hr',\n 'img',\n 'li',\n 'main',\n 'nav',\n 'ol',\n 'p',\n 'pre',\n 'section',\n 'table',\n 'ul',\n])\n\nconst BLOCK_SELECTOR = Array.from(BLOCK_TAGS).join(', ')\n\nconst isBlockElement = (node: Node): node is Element =>\n node.nodeType === Node.ELEMENT_NODE &&\n BLOCK_TAGS.has((node as Element).tagName.toLowerCase())\n\n/**\n * An inline-tagged element hiding block content — e.g. Google Docs wraps\n * the whole clipboard in `<b style=\"font-weight:normal\">`. Treated as a\n * wrapper to recurse through, never as one giant mark.\n */\nconst isInlineWrapperOfBlocks = (node: Node): node is Element =>\n node.nodeType === Node.ELEMENT_NODE &&\n !isBlockElement(node) &&\n (node as Element).querySelector(BLOCK_SELECTOR) != null\n\n/**\n * Merges adjacent same-type runs (same href for links), re-collapses\n * whitespace across the merge seams and trims the ends of the sequence.\n */\nfunction finishInlines(inlines: MarkdownInline[]): MarkdownInline[] {\n const merged: MarkdownInline[] = []\n for (const inline of inlines) {\n if (!inline.text) continue\n const last = merged[merged.length - 1]\n if (\n last &&\n last.type === inline.type &&\n (last.type !== 'link' ||\n last.href === (inline as { href?: string }).href)\n ) {\n last.text = collapseWhitespace(last.text + inline.text)\n } else {\n merged.push({ ...inline })\n }\n }\n const first = merged[0]\n if (first) first.text = first.text.replace(/^\\s+/, '')\n const last = merged[merged.length - 1]\n if (last) last.text = last.text.replace(/\\s+$/, '')\n return merged.filter((inline) => inline.text.length > 0)\n}\n\nfunction inlinesFromNodes(nodes: Node[]): MarkdownInline[] {\n const out: MarkdownInline[] = []\n const pushText = (text: string): void => {\n if (text) out.push({ type: 'text', text })\n }\n /**\n * Marked elements flatten to their whole text content (the dialect has\n * no nested marks), with edge whitespace moved OUT of the mark so the\n * separating spaces around `<strong>`/`<a>` survive serialization.\n */\n const pushMarked = (\n el: Element,\n build: (text: string) => MarkdownInline,\n ): void => {\n const text = collapseWhitespace(el.textContent ?? '')\n const core = text.trim()\n if (text.startsWith(' ')) pushText(' ')\n if (core) out.push(build(core))\n if (core && text.endsWith(' ')) pushText(' ')\n }\n const visit = (node: Node): void => {\n if (node.nodeType === Node.TEXT_NODE) {\n pushText(collapseWhitespace(node.textContent ?? ''))\n return\n }\n if (node.nodeType !== Node.ELEMENT_NODE) return\n const el = node as Element\n const tag = el.tagName.toLowerCase()\n if (tag === 'script' || tag === 'style') return\n if (tag === 'br') {\n pushText(' ')\n return\n }\n if (tag === 'strong' || tag === 'b') {\n pushMarked(el, (text) => ({ type: 'bold', text }))\n return\n }\n if (tag === 'em' || tag === 'i') {\n pushMarked(el, (text) => ({ type: 'italic', text }))\n return\n }\n if (tag === 'a' && el.getAttribute('href')) {\n // The RAW attribute (not el.href) so site paths like /pricing\n // survive instead of resolving against the parser's base URL.\n const href = el.getAttribute('href') ?? ''\n pushMarked(el, (text) => ({ type: 'link', text, href }))\n return\n }\n Array.from(el.childNodes).forEach(visit)\n }\n nodes.forEach(visit)\n return finishInlines(out)\n}\n\n/** Converts an element's children to markdown-lite inlines. */\nexport function htmlToInlines(node: Node): MarkdownInline[] {\n return inlinesFromNodes(Array.from(node.childNodes))\n}\n\n/**\n * Converts a `text/html` clipboard payload to editor rows. Unknown\n * wrappers recurse so they never swallow content; rows with no inlines\n * and no image are dropped.\n */\nexport function htmlToRows(html: string): EditorRow[] {\n const doc = new DOMParser().parseFromString(html, 'text/html')\n const rows: EditorRow[] = []\n const pushTextRow = (\n kind:\n | 'paragraph'\n | 'heading2'\n | 'heading3'\n | 'listItem'\n | 'orderedItem'\n | 'quote',\n inlines: MarkdownInline[],\n /** Seeds a numbered run from `<ol start>` (AGL-1320). */\n start?: number,\n ): void => {\n if (inlines.length === 0) return\n rows.push({\n key: nextHtmlRowKey(),\n kind,\n inlines,\n ...(start == null ? {} : { start }),\n })\n }\n const visitBlock = (el: Element): void => {\n const tag = el.tagName.toLowerCase()\n if (tag === 'h1' || tag === 'h2') {\n pushTextRow('heading2', htmlToInlines(el))\n return\n }\n if (tag === 'h3' || tag === 'h4' || tag === 'h5' || tag === 'h6') {\n pushTextRow('heading3', htmlToInlines(el))\n return\n }\n if (tag === 'ul' || tag === 'ol') {\n // An `<ol>` becomes numbered rows (AGL-1320), carrying `start` on the\n // first item that actually lands — an empty leading `<li>` produces no\n // row, and the run has to start counting from the one that does.\n const ordered = tag === 'ol'\n const startAttr = el.getAttribute('start')\n const parsedStart = Number(startAttr)\n let pending = !ordered\n ? undefined\n : startAttr && Number.isFinite(parsedStart)\n ? Math.max(0, Math.trunc(parsedStart))\n : 1\n for (const child of Array.from(el.children)) {\n const childTag = child.tagName.toLowerCase()\n if (childTag === 'li') {\n const before = rows.length\n pushTextRow(\n ordered ? 'orderedItem' : 'listItem',\n htmlToInlines(child),\n pending,\n )\n if (rows.length > before) pending = undefined\n } else if (childTag === 'ul' || childTag === 'ol') visitBlock(child)\n }\n return\n }\n if (tag === 'li') {\n pushTextRow('listItem', htmlToInlines(el))\n return\n }\n // A GitHub README's two workhorse blocks (AGL-981). `<pre>` keeps its\n // line breaks verbatim — the whole point of a snippet — and the\n // language comes off the `language-x` class GitHub puts on the inner\n // `<code>`.\n if (tag === 'pre') {\n const text = (el.textContent ?? '')\n .replace(/^(?:[ \\t]*\\n)+/, '')\n .replace(/\\s+$/, '')\n if (text.trim()) {\n const code = el.querySelector('code')\n const lang = /(?:^|\\s)(?:language|lang)-([\\w+#.-]+)/.exec(\n code?.getAttribute('class') ?? '',\n )\n rows.push({\n key: nextHtmlRowKey(),\n kind: 'code',\n lang: lang?.[1] ?? '',\n text,\n })\n }\n return\n }\n if (tag === 'table') {\n const cellsOf = (tr: Element): Element[] =>\n Array.from(tr.children).filter((cell) =>\n /^t[hd]$/.test(cell.tagName.toLowerCase()),\n )\n const cellRows = Array.from(el.querySelectorAll('tr'))\n // A nested table's rows belong to IT, not to this one; its cells\n // still flatten into the containing cell's inlines, since the\n // dialect has no nesting.\n .filter((tr) => tr.closest('table') === el)\n .map(cellsOf)\n .filter((cells) => cells.length > 0)\n const [header, ...body] = cellRows\n if (header) {\n rows.push({\n key: nextHtmlRowKey(),\n kind: 'table',\n // GitHub writes a column's alignment onto every cell of it.\n align: header.map((cell) => {\n const align =\n cell.getAttribute('align') ??\n (cell as HTMLElement).style?.textAlign\n return align === 'center' || align === 'right' ? align : 'left'\n }),\n header: header.map((cell) => htmlToInlines(cell)),\n // Squared off to the header, the shape the parser guarantees.\n rows: body.map((cells) =>\n header.map((_, index) => {\n const cell = cells[index]\n return cell ? htmlToInlines(cell) : []\n }),\n ),\n })\n }\n return\n }\n if (tag === 'img') {\n const src = el.getAttribute('src')\n if (src) {\n rows.push({\n key: nextHtmlRowKey(),\n kind: 'image',\n src,\n alt: el.getAttribute('alt') ?? '',\n })\n }\n return\n }\n // A quote keeps its quoteness (AGL-1315): each paragraph of the\n // blockquote becomes its own quote row; a flat blockquote is one.\n if (tag === 'blockquote') {\n const paragraphs = Array.from(el.querySelectorAll('p'))\n if (paragraphs.length) {\n for (const p of paragraphs) pushTextRow('quote', htmlToInlines(p))\n } else {\n pushTextRow('quote', htmlToInlines(el))\n }\n return\n }\n if (tag === 'p' || tag === 'div' || tag === 'section') {\n // A container with block children is a wrapper — recurse so the\n // inner structure survives; otherwise it is one paragraph.\n const wrapsBlocks = Array.from(el.children).some(isBlockElement)\n if (wrapsBlocks) visitChildren(el)\n else pushTextRow('paragraph', htmlToInlines(el))\n return\n }\n // Unknown block-ish element (figure, table, article…): recurse.\n visitChildren(el)\n }\n /**\n * Walks mixed content: consecutive inline nodes (bare text, marks,\n * spans) group into ONE paragraph row; block elements flush the run\n * and map to their own rows.\n */\n const visitChildren = (parent: Node): void => {\n let pendingInline: Node[] = []\n const flush = (): void => {\n if (pendingInline.length === 0) return\n pushTextRow('paragraph', inlinesFromNodes(pendingInline))\n pendingInline = []\n }\n for (const child of Array.from(parent.childNodes)) {\n if (isBlockElement(child)) {\n flush()\n visitBlock(child)\n } else if (isInlineWrapperOfBlocks(child)) {\n flush()\n visitChildren(child)\n } else {\n pendingInline.push(child)\n }\n }\n flush()\n }\n visitChildren(doc.body)\n return rows\n}\n"],"names":["htmlRowSeq","nextHtmlRowKey","collapseWhitespace","text","replace","BLOCK_TAGS","Set","BLOCK_SELECTOR","Array","from","join","isBlockElement","node","nodeType","Node","ELEMENT_NODE","has","tagName","toLowerCase","isInlineWrapperOfBlocks","querySelector","finishInlines","inlines","merged","inline","last","length","type","href","push","first","filter","inlinesFromNodes","nodes","out","pushText","pushMarked","el","build","textContent","core","trim","startsWith","endsWith","visit","TEXT_NODE","tag","getAttribute","childNodes","forEach","htmlToInlines","htmlToRows","html","doc","DOMParser","parseFromString","rows","pushTextRow","kind","start","key","visitBlock","ordered","startAttr","parsedStart","Number","pending","undefined","isFinite","Math","max","trunc","child","children","childTag","before","code","lang","exec","cellsOf","tr","cell","test","cellRows","querySelectorAll","closest","map","cells","header","body","align","style","textAlign","_","index","src","alt","paragraphs","p","wrapsBlocks","some","visitChildren","parent","pendingInline","flush"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ export interface MarkdownLink {
2
+ /** A reference (`entry:…`, `screen:…`) or a URL the dialect accepts. */
3
+ href: string;
4
+ text: string;
5
+ }
6
+ export interface MarkdownLinkDialogProps {
7
+ open: boolean;
8
+ /** The link being edited; absent when a new one is being inserted. */
9
+ href?: string;
10
+ /** Ask for the link's text too, seeded with this (the selection). */
11
+ withText?: boolean;
12
+ text?: string;
13
+ onClose: () => void;
14
+ onConfirm: (link: MarkdownLink) => void;
15
+ }
16
+ /**
17
+ * Where one link goes, as a sentence: a URL as written, and a reference by
18
+ * its target's readable name (AGL-3119). `entry:Hq3…/9fK…` tells an author
19
+ * nothing about the post they linked, and the name is what they would check.
20
+ *
21
+ * Used by the visual editor's link popover, and lives here because naming a
22
+ * target is the same job this dialog does when it opens on one.
23
+ */
24
+ export declare function MarkdownLinkTargetName({ href }: {
25
+ href: string;
26
+ }): import("react").JSX.Element;
27
+ export declare namespace MarkdownLinkTargetName {
28
+ var displayName: string;
29
+ }
30
+ /**
31
+ * Insert or edit one link. The body mounts when the dialog opens — a Dialog
32
+ * drops its children once closed — so the fields always start from `href`
33
+ * and `text` rather than from whatever the last link left in them.
34
+ */
35
+ export declare function MarkdownLinkDialog(props: MarkdownLinkDialogProps): import("react").JSX.Element;
36
+ export declare namespace MarkdownLinkDialog {
37
+ var displayName: string;
38
+ }
39
+ export default MarkdownLinkDialog;
@@ -0,0 +1,195 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ /**
20
+ * The markdown editor's link dialog (AGL-3119), shared by the visual surface
21
+ * and the raw-source surface behind the one toolbar.
22
+ *
23
+ * A link in an entry body used to be a typed address, so a post could link
24
+ * another post only as `/blog/old-slug` — which the next rename breaks. Where
25
+ * the page knows the site (a routing map in `ScreenLinkContext`, entries
26
+ * through `LinkTargetSearchContext`), the dialog leads with the same lookup the
27
+ * designer's link pickers use and inserts a REFERENCE — `[text](entry:c/e)`,
28
+ * `screen:`, `collection:`, `feed:` — which renderers resolve to wherever the
29
+ * target lives now. A typed URL stays one choice away. Where the page knows
30
+ * nothing (a marketplace README), it is the URL box it always was.
31
+ */ import { formatCollectionLinkValue, formatScreenLinkValue, isMarkdownLinkReference, isSupportedLinkHref, useLinkTargetLabel, LinkTargetSearchContext, linkTargetKind, parseFeedLinkValue, parseScreenLinkValue, resolveScreenHref, ScreenLinkContext, screenLinkTargetOptions } from "@aglyn/aglyn";
32
+ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from "@mui/material";
33
+ import { useContext, useMemo, useState } from "react";
34
+ import { EXTERNAL_URL_OPTION, LinkTargetAutocomplete } from "./link-target-autocomplete.component.js";
35
+ const EXTERNAL_CHOICE = [
36
+ {
37
+ value: EXTERNAL_URL_OPTION,
38
+ label: 'External URL or path…'
39
+ }
40
+ ];
41
+ /**
42
+ * Where one link goes, as a sentence: a URL as written, and a reference by
43
+ * its target's readable name (AGL-3119). `entry:Hq3…/9fK…` tells an author
44
+ * nothing about the post they linked, and the name is what they would check.
45
+ *
46
+ * Used by the visual editor's link popover, and lives here because naming a
47
+ * target is the same job this dialog does when it opens on one.
48
+ */ export function MarkdownLinkTargetName({ href }) {
49
+ const reference = isMarkdownLinkReference(href) ? parseScreenLinkValue(href) : undefined;
50
+ const target = useLinkTargetLabel(reference);
51
+ return /*#__PURE__*/ _jsx(_Fragment, {
52
+ children: target ? `Links to ${target.label}` : href
53
+ });
54
+ }
55
+ MarkdownLinkTargetName.displayName = 'MarkdownLinkTargetName';
56
+ /**
57
+ * What a link to a target says when the author wrote nothing: the name the
58
+ * site already gives it, or its address — never its stored reference, which
59
+ * would put `entry:Hq3…/9fK…` in the middle of a sentence.
60
+ */ function targetText(key, screens, labels, entryTitle) {
61
+ const address = resolveScreenHref(screens, key);
62
+ switch(linkTargetKind(key)){
63
+ case 'entry':
64
+ return (entryTitle == null ? void 0 : entryTitle.trim()) || 'this entry';
65
+ case 'feed':
66
+ {
67
+ var _parseFeedLinkValue, _ref;
68
+ const collectionId = (_parseFeedLinkValue = parseFeedLinkValue(key)) != null ? _parseFeedLinkValue : '';
69
+ const name = (_ref = labels == null ? void 0 : labels[key]) != null ? _ref : labels == null ? void 0 : labels[formatCollectionLinkValue(collectionId)];
70
+ return name ? `${name} RSS feed` : address != null ? address : 'this feed';
71
+ }
72
+ default:
73
+ var _ref1, _ref2;
74
+ return (_ref1 = (_ref2 = labels == null ? void 0 : labels[key]) != null ? _ref2 : address) != null ? _ref1 : 'this page';
75
+ }
76
+ }
77
+ /** The dialog's body, mounted per opening so every opening starts fresh. */ function MarkdownLinkForm(props) {
78
+ const { href, withText, text: initialText, onClose, onConfirm } = props;
79
+ const { screens, labels } = useContext(ScreenLinkContext);
80
+ const search = useContext(LinkTargetSearchContext);
81
+ // A lookup needs something to look in: a routing map or a search provider.
82
+ const lookup = screens !== undefined || search.available;
83
+ const editing = href !== undefined;
84
+ const initialKey = href ? parseScreenLinkValue(href) : undefined;
85
+ const [choice, setChoice] = useState(()=>initialKey != null ? initialKey : href || !lookup ? EXTERNAL_URL_OPTION : null);
86
+ const [url, setUrl] = useState(()=>initialKey ? '' : href != null ? href : '');
87
+ const [text, setText] = useState(initialText != null ? initialText : '');
88
+ const [entryTitle, setEntryTitle] = useState();
89
+ const targets = useMemo(()=>screenLinkTargetOptions(screens, labels, 'label'), [
90
+ screens,
91
+ labels
92
+ ]);
93
+ const external = choice === EXTERNAL_URL_OPTION;
94
+ const nextHref = external ? url.trim() : choice ? formatScreenLinkValue(choice) : '';
95
+ const valid = external ? isSupportedLinkHref(nextHref) : isMarkdownLinkReference(nextHref);
96
+ const confirm = ()=>{
97
+ if (!valid) return;
98
+ const fallback = external ? nextHref : targetText(choice != null ? choice : '', screens, labels, entryTitle);
99
+ onConfirm({
100
+ href: nextHref,
101
+ text: text.trim() || fallback
102
+ });
103
+ };
104
+ const handleChoice = (next, detail)=>{
105
+ setChoice(next);
106
+ if (detail.address !== undefined) setUrl(detail.address);
107
+ setEntryTitle(detail.title);
108
+ };
109
+ return /*#__PURE__*/ _jsxs(_Fragment, {
110
+ children: [
111
+ /*#__PURE__*/ _jsx(DialogTitle, {
112
+ children: editing ? 'Edit link' : 'Insert link'
113
+ }),
114
+ /*#__PURE__*/ _jsxs(DialogContent, {
115
+ sx: {
116
+ display: 'flex',
117
+ flexDirection: 'column',
118
+ gap: 2,
119
+ pt: 1
120
+ },
121
+ children: [
122
+ lookup ? /*#__PURE__*/ _jsx(LinkTargetAutocomplete, {
123
+ label: "Link to",
124
+ placeholder: "Search pages and entries",
125
+ value: choice,
126
+ onChange: handleChoice,
127
+ targets: targets,
128
+ trailing: EXTERNAL_CHOICE,
129
+ addressOption: EXTERNAL_URL_OPTION,
130
+ autoFocus: !external,
131
+ openOnFocus: !editing,
132
+ helperText: external ? undefined : 'A page or entry picked here keeps working when it is renamed.'
133
+ }) : null,
134
+ external ? /*#__PURE__*/ _jsx(TextField, {
135
+ label: "URL",
136
+ value: url,
137
+ onChange: (event)=>setUrl(event.target.value),
138
+ onKeyDown: (event)=>{
139
+ if (event.key === 'Enter' && valid) {
140
+ event.preventDefault();
141
+ confirm();
142
+ }
143
+ },
144
+ size: "small",
145
+ autoFocus: true,
146
+ sx: lookup ? undefined : {
147
+ mt: 1
148
+ },
149
+ helperText: "https:// or a site path like /pricing"
150
+ }) : null,
151
+ withText ? /*#__PURE__*/ _jsx(TextField, {
152
+ label: "Text",
153
+ value: text,
154
+ onChange: (event)=>setText(event.target.value),
155
+ size: "small",
156
+ helperText: external ? 'Falls back to the URL when blank' : 'Falls back to the page or entry name when blank'
157
+ }) : null
158
+ ]
159
+ }),
160
+ /*#__PURE__*/ _jsxs(DialogActions, {
161
+ children: [
162
+ /*#__PURE__*/ _jsx(Button, {
163
+ onClick: onClose,
164
+ children: 'Cancel'
165
+ }),
166
+ /*#__PURE__*/ _jsx(Button, {
167
+ variant: "contained",
168
+ color: "primary",
169
+ disabled: !valid,
170
+ onClick: confirm,
171
+ children: editing ? 'Save' : 'Insert'
172
+ })
173
+ ]
174
+ })
175
+ ]
176
+ });
177
+ }
178
+ /**
179
+ * Insert or edit one link. The body mounts when the dialog opens — a Dialog
180
+ * drops its children once closed — so the fields always start from `href`
181
+ * and `text` rather than from whatever the last link left in them.
182
+ */ export function MarkdownLinkDialog(props) {
183
+ const { open, onClose } = props;
184
+ return /*#__PURE__*/ _jsx(Dialog, {
185
+ open: open,
186
+ onClose: onClose,
187
+ maxWidth: "xs",
188
+ fullWidth: true,
189
+ children: /*#__PURE__*/ _jsx(MarkdownLinkForm, _extends({}, props))
190
+ });
191
+ }
192
+ MarkdownLinkDialog.displayName = 'MarkdownLinkDialog';
193
+ export default MarkdownLinkDialog;
194
+
195
+ //# sourceMappingURL=markdown-link-dialog.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../libs/aglyn-markdown-editor/src/lib/markdown-link-dialog.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * The markdown editor's link dialog (AGL-3119), shared by the visual surface\n * and the raw-source surface behind the one toolbar.\n *\n * A link in an entry body used to be a typed address, so a post could link\n * another post only as `/blog/old-slug` — which the next rename breaks. Where\n * the page knows the site (a routing map in `ScreenLinkContext`, entries\n * through `LinkTargetSearchContext`), the dialog leads with the same lookup the\n * designer's link pickers use and inserts a REFERENCE — `[text](entry:c/e)`,\n * `screen:`, `collection:`, `feed:` — which renderers resolve to wherever the\n * target lives now. A typed URL stays one choice away. Where the page knows\n * nothing (a marketplace README), it is the URL box it always was.\n */\n\nimport {\n formatCollectionLinkValue,\n formatScreenLinkValue,\n isMarkdownLinkReference,\n isSupportedLinkHref,\n useLinkTargetLabel,\n LinkTargetSearchContext,\n linkTargetKind,\n parseFeedLinkValue,\n parseScreenLinkValue,\n resolveScreenHref,\n ScreenLinkContext,\n screenLinkTargetOptions,\n} from '@aglyn/aglyn'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n TextField,\n} from '@mui/material'\nimport { useContext, useMemo, useState } from 'react'\nimport {\n EXTERNAL_URL_OPTION,\n LinkTargetAutocomplete,\n type LinkTargetChangeDetail,\n type LinkTargetChoice,\n} from './link-target-autocomplete.component'\n\nexport interface MarkdownLink {\n /** A reference (`entry:…`, `screen:…`) or a URL the dialect accepts. */\n href: string\n text: string\n}\n\nexport interface MarkdownLinkDialogProps {\n open: boolean\n /** The link being edited; absent when a new one is being inserted. */\n href?: string\n /** Ask for the link's text too, seeded with this (the selection). */\n withText?: boolean\n text?: string\n onClose: () => void\n onConfirm: (link: MarkdownLink) => void\n}\n\nconst EXTERNAL_CHOICE: readonly LinkTargetChoice[] = [\n { value: EXTERNAL_URL_OPTION, label: 'External URL or path…' },\n]\n\n/**\n * Where one link goes, as a sentence: a URL as written, and a reference by\n * its target's readable name (AGL-3119). `entry:Hq3…/9fK…` tells an author\n * nothing about the post they linked, and the name is what they would check.\n *\n * Used by the visual editor's link popover, and lives here because naming a\n * target is the same job this dialog does when it opens on one.\n */\nexport function MarkdownLinkTargetName({ href }: { href: string }) {\n const reference = isMarkdownLinkReference(href)\n ? parseScreenLinkValue(href)\n : undefined\n const target = useLinkTargetLabel(reference)\n return <>{target ? `Links to ${target.label}` : href}</>\n}\nMarkdownLinkTargetName.displayName = 'MarkdownLinkTargetName'\n\n/**\n * What a link to a target says when the author wrote nothing: the name the\n * site already gives it, or its address — never its stored reference, which\n * would put `entry:Hq3…/9fK…` in the middle of a sentence.\n */\nfunction targetText(\n key: string,\n screens: Record<string, string> | undefined,\n labels: Record<string, string> | undefined,\n entryTitle: string | undefined,\n): string {\n const address = resolveScreenHref(screens, key)\n switch (linkTargetKind(key)) {\n case 'entry':\n return entryTitle?.trim() || 'this entry'\n case 'feed': {\n const collectionId = parseFeedLinkValue(key) ?? ''\n const name =\n labels?.[key] ?? labels?.[formatCollectionLinkValue(collectionId)]\n return name ? `${name} RSS feed` : (address ?? 'this feed')\n }\n default:\n return labels?.[key] ?? address ?? 'this page'\n }\n}\n\n/** The dialog's body, mounted per opening so every opening starts fresh. */\nfunction MarkdownLinkForm(props: MarkdownLinkDialogProps) {\n const { href, withText, text: initialText, onClose, onConfirm } = props\n const { screens, labels } = useContext(ScreenLinkContext)\n const search = useContext(LinkTargetSearchContext)\n // A lookup needs something to look in: a routing map or a search provider.\n const lookup = screens !== undefined || search.available\n const editing = href !== undefined\n const initialKey = href ? parseScreenLinkValue(href) : undefined\n const [choice, setChoice] = useState<string | null>(() =>\n initialKey ?? (href || !lookup ? EXTERNAL_URL_OPTION : null),\n )\n const [url, setUrl] = useState(() => (initialKey ? '' : (href ?? '')))\n const [text, setText] = useState(initialText ?? '')\n const [entryTitle, setEntryTitle] = useState<string | undefined>()\n const targets = useMemo(\n () => screenLinkTargetOptions(screens, labels, 'label'),\n [screens, labels],\n )\n\n const external = choice === EXTERNAL_URL_OPTION\n const nextHref = external\n ? url.trim()\n : choice\n ? formatScreenLinkValue(choice)\n : ''\n const valid = external\n ? isSupportedLinkHref(nextHref)\n : isMarkdownLinkReference(nextHref)\n\n const confirm = () => {\n if (!valid) return\n const fallback = external\n ? nextHref\n : targetText(choice ?? '', screens, labels, entryTitle)\n onConfirm({ href: nextHref, text: text.trim() || fallback })\n }\n\n const handleChoice = (next: string, detail: LinkTargetChangeDetail) => {\n setChoice(next)\n if (detail.address !== undefined) setUrl(detail.address)\n setEntryTitle(detail.title)\n }\n\n return (\n <>\n <DialogTitle>{editing ? 'Edit link' : 'Insert link'}</DialogTitle>\n <DialogContent\n sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 1 }}\n >\n {lookup ? (\n <LinkTargetAutocomplete\n label=\"Link to\"\n placeholder=\"Search pages and entries\"\n value={choice}\n onChange={handleChoice}\n targets={targets}\n trailing={EXTERNAL_CHOICE}\n addressOption={EXTERNAL_URL_OPTION}\n autoFocus={!external}\n openOnFocus={!editing}\n helperText={\n external\n ? undefined\n : 'A page or entry picked here keeps working when it is renamed.'\n }\n />\n ) : null}\n {external ? (\n <TextField\n label=\"URL\"\n value={url}\n onChange={(event) => setUrl(event.target.value)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' && valid) {\n event.preventDefault()\n confirm()\n }\n }}\n size=\"small\"\n autoFocus\n sx={lookup ? undefined : { mt: 1 }}\n helperText=\"https:// or a site path like /pricing\"\n />\n ) : null}\n {withText ? (\n <TextField\n label=\"Text\"\n value={text}\n onChange={(event) => setText(event.target.value)}\n size=\"small\"\n helperText={\n external\n ? 'Falls back to the URL when blank'\n : 'Falls back to the page or entry name when blank'\n }\n />\n ) : null}\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!valid}\n onClick={confirm}\n >\n {editing ? 'Save' : 'Insert'}\n </Button>\n </DialogActions>\n </>\n )\n}\n\n/**\n * Insert or edit one link. The body mounts when the dialog opens — a Dialog\n * drops its children once closed — so the fields always start from `href`\n * and `text` rather than from whatever the last link left in them.\n */\nexport function MarkdownLinkDialog(props: MarkdownLinkDialogProps) {\n const { open, onClose } = props\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <MarkdownLinkForm {...props} />\n </Dialog>\n )\n}\nMarkdownLinkDialog.displayName = 'MarkdownLinkDialog'\n\nexport default MarkdownLinkDialog\n"],"names":["formatCollectionLinkValue","formatScreenLinkValue","isMarkdownLinkReference","isSupportedLinkHref","useLinkTargetLabel","LinkTargetSearchContext","linkTargetKind","parseFeedLinkValue","parseScreenLinkValue","resolveScreenHref","ScreenLinkContext","screenLinkTargetOptions","Button","Dialog","DialogActions","DialogContent","DialogTitle","TextField","useContext","useMemo","useState","EXTERNAL_URL_OPTION","LinkTargetAutocomplete","EXTERNAL_CHOICE","value","label","MarkdownLinkTargetName","href","reference","undefined","target","displayName","targetText","key","screens","labels","entryTitle","address","trim","collectionId","name","MarkdownLinkForm","props","withText","text","initialText","onClose","onConfirm","search","lookup","available","editing","initialKey","choice","setChoice","url","setUrl","setText","setEntryTitle","targets","external","nextHref","valid","confirm","fallback","handleChoice","next","detail","title","sx","display","flexDirection","gap","pt","placeholder","onChange","trailing","addressOption","autoFocus","openOnFocus","helperText","event","onKeyDown","preventDefault","size","mt","onClick","variant","color","disabled","MarkdownLinkDialog","open","maxWidth","fullWidth"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export interface MarkdownLiteViewProps {
2
+ /** Markdown-lite source. */
3
+ source: string;
4
+ }
5
+ export declare function MarkdownLiteView({ source }: MarkdownLiteViewProps): import("react").JSX.Element;
6
+ export default MarkdownLiteView;