@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.
- package/LICENSE +201 -0
- package/README.md +34 -0
- package/package.json +40 -0
- package/src/index.d.ts +43 -0
- package/src/index.js +42 -0
- package/src/index.js.map +1 -0
- package/src/lib/link-target-autocomplete.component.d.ts +60 -0
- package/src/lib/link-target-autocomplete.component.js +235 -0
- package/src/lib/link-target-autocomplete.component.js.map +1 -0
- package/src/lib/markdown-editor-toolbar.component.d.ts +14 -0
- package/src/lib/markdown-editor-toolbar.component.js +254 -0
- package/src/lib/markdown-editor-toolbar.component.js.map +1 -0
- package/src/lib/markdown-field.component.d.ts +43 -0
- package/src/lib/markdown-field.component.js +144 -0
- package/src/lib/markdown-field.component.js.map +1 -0
- package/src/lib/markdown-html-paste.d.ts +38 -0
- package/src/lib/markdown-html-paste.js +312 -0
- package/src/lib/markdown-html-paste.js.map +1 -0
- package/src/lib/markdown-link-dialog.component.d.ts +39 -0
- package/src/lib/markdown-link-dialog.component.js +195 -0
- package/src/lib/markdown-link-dialog.component.js.map +1 -0
- package/src/lib/markdown-lite-view.component.d.ts +6 -0
- package/src/lib/markdown-lite-view.component.js +243 -0
- package/src/lib/markdown-lite-view.component.js.map +1 -0
- package/src/lib/markdown-source-command.d.ts +51 -0
- package/src/lib/markdown-source-command.js +56 -0
- package/src/lib/markdown-source-command.js.map +1 -0
- package/src/lib/markdown-visual-editor.component.d.ts +109 -0
- package/src/lib/markdown-visual-editor.component.js +2019 -0
- 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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"}
|