@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,243 @@
|
|
|
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { HEADING_ANCHOR_SCROLL_MARGIN, isMarkdownLinkReference, LinkTargetSearchContext, linkTargetKind, markdownHeadingSlugs, parseMarkdownLite, parseScreenLinkValue, resolveMediaSrc, useLinkTargetLabel } from "@aglyn/aglyn";
|
|
19
|
+
import { Box, Link as MuiLink, Stack, Typography } from "@mui/material";
|
|
20
|
+
import { useContext, useMemo } from "react";
|
|
21
|
+
/** What a reference names, for a surface that cannot resolve it. */ const REFERENCE_NOUNS = {
|
|
22
|
+
screen: 'a page on this site',
|
|
23
|
+
collection: 'a collection listing on this site',
|
|
24
|
+
feed: 'a collection feed on this site',
|
|
25
|
+
entry: 'an entry on this site'
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A link that names its target by reference (AGL-3118) as TEXT, not an
|
|
29
|
+
* anchor: `entry:Hq3…/9fK…` is not an address, so an `<a href>` built from it
|
|
30
|
+
* is a link that goes nowhere. This preview belongs to the console, which
|
|
31
|
+
* does not serve the site the reference points into; what it owes the reader
|
|
32
|
+
* is what the link says and where it will go, which the title carries —
|
|
33
|
+
* resolved through the routing map and the search seam where a surface
|
|
34
|
+
* provides them.
|
|
35
|
+
*/ function ReferenceLinkText({ href, children }) {
|
|
36
|
+
const key = parseScreenLinkValue(href);
|
|
37
|
+
const search = useContext(LinkTargetSearchContext);
|
|
38
|
+
const target = useLinkTargetLabel(key);
|
|
39
|
+
// Named only where something could name it: with no routing map the label
|
|
40
|
+
// falls back to the key, and with no search seam an entry's is a stand-in.
|
|
41
|
+
const named = target && !target.pending && target.label !== key && (linkTargetKind(key != null ? key : href) !== 'entry' || search.available);
|
|
42
|
+
return /*#__PURE__*/ _jsx(Box, {
|
|
43
|
+
component: "span",
|
|
44
|
+
"data-md-reference": "",
|
|
45
|
+
title: `Links to ${named ? target.label : REFERENCE_NOUNS[linkTargetKind(key != null ? key : href)]}`,
|
|
46
|
+
sx: {
|
|
47
|
+
color: 'primary.main',
|
|
48
|
+
textDecoration: 'underline dotted'
|
|
49
|
+
},
|
|
50
|
+
children: children
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const renderInlines = (inlines)=>inlines.map((inline, index)=>inline.type === 'bold' ? /*#__PURE__*/ _jsx("strong", {
|
|
54
|
+
children: inline.text
|
|
55
|
+
}, index) : inline.type === 'italic' ? /*#__PURE__*/ _jsx("em", {
|
|
56
|
+
children: inline.text
|
|
57
|
+
}, index) : inline.type === 'link' ? isMarkdownLinkReference(inline.href) ? /*#__PURE__*/ _jsx(ReferenceLinkText, {
|
|
58
|
+
href: inline.href,
|
|
59
|
+
children: inline.text
|
|
60
|
+
}, index) : /*#__PURE__*/ _jsx(MuiLink, {
|
|
61
|
+
href: inline.href,
|
|
62
|
+
target: "_blank",
|
|
63
|
+
rel: "noopener noreferrer",
|
|
64
|
+
underline: "hover",
|
|
65
|
+
children: inline.text
|
|
66
|
+
}, index) : /*#__PURE__*/ _jsx("span", {
|
|
67
|
+
children: inline.text
|
|
68
|
+
}, index));
|
|
69
|
+
export function MarkdownLiteView({ source }) {
|
|
70
|
+
const blocks = useMemo(()=>parseMarkdownLite(source), [
|
|
71
|
+
source
|
|
72
|
+
]);
|
|
73
|
+
// The preview stamps the same heading anchors the published page does
|
|
74
|
+
// (AGL-1162), so what an author checks here is what a `#slug` link will
|
|
75
|
+
// find — and so a document whose headings collide is visible before it
|
|
76
|
+
// ships, not after.
|
|
77
|
+
const slugs = useMemo(()=>markdownHeadingSlugs(blocks), [
|
|
78
|
+
blocks
|
|
79
|
+
]);
|
|
80
|
+
return /*#__PURE__*/ _jsx(Stack, {
|
|
81
|
+
spacing: 1.5,
|
|
82
|
+
children: blocks.map((block, index)=>{
|
|
83
|
+
switch(block.type){
|
|
84
|
+
case 'heading':
|
|
85
|
+
return /*#__PURE__*/ _jsx(Typography, {
|
|
86
|
+
id: slugs[index],
|
|
87
|
+
variant: block.level === 2 ? 'h6' : 'subtitle1',
|
|
88
|
+
component: block.level === 2 ? 'h2' : 'h3',
|
|
89
|
+
sx: {
|
|
90
|
+
scrollMarginTop: `${HEADING_ANCHOR_SCROLL_MARGIN}px`
|
|
91
|
+
},
|
|
92
|
+
children: renderInlines(block.inlines)
|
|
93
|
+
}, index);
|
|
94
|
+
case 'image':
|
|
95
|
+
return /*#__PURE__*/ _jsx(Box, {
|
|
96
|
+
component: "img",
|
|
97
|
+
// Resolved (AGL-1686), so the console preview shows the same
|
|
98
|
+
// picture the published page will. No `hostId`: this renders
|
|
99
|
+
// in the console, which has no site context — the scope the
|
|
100
|
+
// picker baked in is what resolves. The console serves
|
|
101
|
+
// `/api/media/cdn/…` itself, so the relative URL this
|
|
102
|
+
// produces is fetchable from here.
|
|
103
|
+
src: resolveMediaSrc(block.src),
|
|
104
|
+
alt: block.alt,
|
|
105
|
+
loading: "lazy",
|
|
106
|
+
sx: {
|
|
107
|
+
maxWidth: '100%',
|
|
108
|
+
borderRadius: 1,
|
|
109
|
+
border: 1,
|
|
110
|
+
borderColor: 'divider'
|
|
111
|
+
}
|
|
112
|
+
}, index);
|
|
113
|
+
case 'list':
|
|
114
|
+
return /*#__PURE__*/ _jsx(Stack, {
|
|
115
|
+
component: "ul",
|
|
116
|
+
spacing: 0.5,
|
|
117
|
+
sx: {
|
|
118
|
+
my: 0
|
|
119
|
+
},
|
|
120
|
+
children: block.items.map((item, itemIndex)=>/*#__PURE__*/ _jsx(Typography, {
|
|
121
|
+
component: "li",
|
|
122
|
+
variant: "body2",
|
|
123
|
+
children: renderInlines(item)
|
|
124
|
+
}, itemIndex))
|
|
125
|
+
}, index);
|
|
126
|
+
// A numbered list (AGL-1320) — a real `<ol>` with `start`, so the
|
|
127
|
+
// console preview counts the same way the published page does.
|
|
128
|
+
case 'orderedList':
|
|
129
|
+
return /*#__PURE__*/ _jsx(Stack, {
|
|
130
|
+
component: "ol",
|
|
131
|
+
start: block.start,
|
|
132
|
+
spacing: 0.5,
|
|
133
|
+
sx: {
|
|
134
|
+
my: 0
|
|
135
|
+
},
|
|
136
|
+
children: block.items.map((item, itemIndex)=>/*#__PURE__*/ _jsx(Typography, {
|
|
137
|
+
component: "li",
|
|
138
|
+
variant: "body2",
|
|
139
|
+
children: renderInlines(item)
|
|
140
|
+
}, itemIndex))
|
|
141
|
+
}, index);
|
|
142
|
+
case 'code':
|
|
143
|
+
return /*#__PURE__*/ _jsx(Box, {
|
|
144
|
+
component: "pre",
|
|
145
|
+
sx: {
|
|
146
|
+
my: 0,
|
|
147
|
+
p: 1.5,
|
|
148
|
+
overflowX: 'auto',
|
|
149
|
+
borderRadius: 1,
|
|
150
|
+
border: 1,
|
|
151
|
+
borderColor: 'divider',
|
|
152
|
+
bgcolor: 'action.hover',
|
|
153
|
+
fontFamily: 'monospace',
|
|
154
|
+
fontSize: 13
|
|
155
|
+
},
|
|
156
|
+
children: /*#__PURE__*/ _jsx("code", {
|
|
157
|
+
children: block.text
|
|
158
|
+
})
|
|
159
|
+
}, index);
|
|
160
|
+
case 'table':
|
|
161
|
+
return /*#__PURE__*/ _jsx(Box, {
|
|
162
|
+
sx: {
|
|
163
|
+
overflowX: 'auto'
|
|
164
|
+
},
|
|
165
|
+
children: /*#__PURE__*/ _jsxs(Box, {
|
|
166
|
+
component: "table",
|
|
167
|
+
sx: {
|
|
168
|
+
borderCollapse: 'collapse',
|
|
169
|
+
width: '100%',
|
|
170
|
+
'& th, & td': {
|
|
171
|
+
border: 1,
|
|
172
|
+
borderColor: 'divider',
|
|
173
|
+
px: 1,
|
|
174
|
+
py: 0.5
|
|
175
|
+
},
|
|
176
|
+
'& th': {
|
|
177
|
+
bgcolor: 'action.hover'
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
children: [
|
|
181
|
+
/*#__PURE__*/ _jsx("thead", {
|
|
182
|
+
children: /*#__PURE__*/ _jsx("tr", {
|
|
183
|
+
children: block.header.map((cell, cellIndex)=>{
|
|
184
|
+
var _block_align_cellIndex;
|
|
185
|
+
return /*#__PURE__*/ _jsx(Typography, {
|
|
186
|
+
component: "th",
|
|
187
|
+
variant: "body2",
|
|
188
|
+
sx: {
|
|
189
|
+
textAlign: (_block_align_cellIndex = block.align[cellIndex]) != null ? _block_align_cellIndex : 'left',
|
|
190
|
+
fontWeight: 600
|
|
191
|
+
},
|
|
192
|
+
children: renderInlines(cell)
|
|
193
|
+
}, cellIndex);
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
}),
|
|
197
|
+
/*#__PURE__*/ _jsx("tbody", {
|
|
198
|
+
children: block.rows.map((row, rowIndex)=>/*#__PURE__*/ _jsx("tr", {
|
|
199
|
+
children: row.map((cell, cellIndex)=>{
|
|
200
|
+
var _block_align_cellIndex;
|
|
201
|
+
return /*#__PURE__*/ _jsx(Typography, {
|
|
202
|
+
component: "td",
|
|
203
|
+
variant: "body2",
|
|
204
|
+
sx: {
|
|
205
|
+
textAlign: (_block_align_cellIndex = block.align[cellIndex]) != null ? _block_align_cellIndex : 'left'
|
|
206
|
+
},
|
|
207
|
+
children: renderInlines(cell)
|
|
208
|
+
}, cellIndex);
|
|
209
|
+
})
|
|
210
|
+
}, rowIndex))
|
|
211
|
+
})
|
|
212
|
+
]
|
|
213
|
+
})
|
|
214
|
+
}, index);
|
|
215
|
+
// A quote (AGL-1315): italic behind a left accent, the muted
|
|
216
|
+
// console idiom of the tenant's pull-quote.
|
|
217
|
+
case 'quote':
|
|
218
|
+
return /*#__PURE__*/ _jsx(Typography, {
|
|
219
|
+
component: "blockquote",
|
|
220
|
+
variant: "body2",
|
|
221
|
+
sx: {
|
|
222
|
+
my: 0,
|
|
223
|
+
mx: 0,
|
|
224
|
+
pl: 1.5,
|
|
225
|
+
borderLeft: 3,
|
|
226
|
+
borderColor: 'divider',
|
|
227
|
+
fontStyle: 'italic',
|
|
228
|
+
color: 'text.secondary'
|
|
229
|
+
},
|
|
230
|
+
children: renderInlines(block.inlines)
|
|
231
|
+
}, index);
|
|
232
|
+
default:
|
|
233
|
+
return /*#__PURE__*/ _jsx(Typography, {
|
|
234
|
+
variant: "body2",
|
|
235
|
+
children: renderInlines(block.inlines)
|
|
236
|
+
}, index);
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
export default MarkdownLiteView;
|
|
242
|
+
|
|
243
|
+
//# sourceMappingURL=markdown-lite-view.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../libs/aglyn-markdown-editor/src/lib/markdown-lite-view.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 * Read-only markdown-lite rendering for console surfaces (AGL-989) — the\n * marketplace listing renderer lives behind the `aglyn:addons` boundary and\n * cannot be imported here.\n *\n * The parser only ever emits text / bold / italic / http(s)-or-site links /\n * images / list items / code text / table cells, so publisher-written\n * markdown cannot inject markup or a javascript: URL: no HTML string is\n * built, and every node below is an element this file chose.\n */\n\nimport type * as Aglyn from '@aglyn/aglyn'\nimport {\n HEADING_ANCHOR_SCROLL_MARGIN,\n isMarkdownLinkReference,\n LinkTargetSearchContext,\n linkTargetKind,\n markdownHeadingSlugs,\n parseMarkdownLite,\n parseScreenLinkValue,\n resolveMediaSrc,\n useLinkTargetLabel,\n} from '@aglyn/aglyn'\nimport { Box, Link as MuiLink, Stack, Typography } from '@mui/material'\nimport { useContext, useMemo, type ReactNode } from 'react'\n\n/** What a reference names, for a surface that cannot resolve it. */\nconst REFERENCE_NOUNS: Readonly<Record<Aglyn.LinkTargetKind, string>> = {\n screen: 'a page on this site',\n collection: 'a collection listing on this site',\n feed: 'a collection feed on this site',\n entry: 'an entry on this site',\n}\n\n/**\n * A link that names its target by reference (AGL-3118) as TEXT, not an\n * anchor: `entry:Hq3…/9fK…` is not an address, so an `<a href>` built from it\n * is a link that goes nowhere. This preview belongs to the console, which\n * does not serve the site the reference points into; what it owes the reader\n * is what the link says and where it will go, which the title carries —\n * resolved through the routing map and the search seam where a surface\n * provides them.\n */\nfunction ReferenceLinkText({\n href,\n children,\n}: {\n href: string\n children: ReactNode\n}) {\n const key = parseScreenLinkValue(href)\n const search = useContext(LinkTargetSearchContext)\n const target = useLinkTargetLabel(key)\n // Named only where something could name it: with no routing map the label\n // falls back to the key, and with no search seam an entry's is a stand-in.\n const named =\n target &&\n !target.pending &&\n target.label !== key &&\n (linkTargetKind(key ?? href) !== 'entry' || search.available)\n return (\n <Box\n component=\"span\"\n data-md-reference=\"\"\n title={`Links to ${\n named ? target.label : REFERENCE_NOUNS[linkTargetKind(key ?? href)]\n }`}\n sx={{ color: 'primary.main', textDecoration: 'underline dotted' }}\n >\n {children}\n </Box>\n )\n}\n\nconst renderInlines = (inlines: Aglyn.MarkdownInline[]) =>\n inlines.map((inline, index) =>\n inline.type === 'bold' ? (\n <strong key={index}>{inline.text}</strong>\n ) : inline.type === 'italic' ? (\n <em key={index}>{inline.text}</em>\n ) : inline.type === 'link' ? (\n isMarkdownLinkReference(inline.href) ? (\n <ReferenceLinkText key={index} href={inline.href}>\n {inline.text}\n </ReferenceLinkText>\n ) : (\n <MuiLink\n key={index}\n href={inline.href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n underline=\"hover\"\n >\n {inline.text}\n </MuiLink>\n )\n ) : (\n <span key={index}>{inline.text}</span>\n ),\n )\n\nexport interface MarkdownLiteViewProps {\n /** Markdown-lite source. */\n source: string\n}\n\nexport function MarkdownLiteView({ source }: MarkdownLiteViewProps) {\n const blocks = useMemo(() => parseMarkdownLite(source), [source])\n // The preview stamps the same heading anchors the published page does\n // (AGL-1162), so what an author checks here is what a `#slug` link will\n // find — and so a document whose headings collide is visible before it\n // ships, not after.\n const slugs = useMemo(() => markdownHeadingSlugs(blocks), [blocks])\n return (\n <Stack spacing={1.5}>\n {blocks.map((block, index) => {\n switch (block.type) {\n case 'heading':\n return (\n <Typography\n key={index}\n id={slugs[index]}\n variant={block.level === 2 ? 'h6' : 'subtitle1'}\n component={block.level === 2 ? 'h2' : 'h3'}\n sx={{\n scrollMarginTop: `${HEADING_ANCHOR_SCROLL_MARGIN}px`,\n }}\n >\n {renderInlines(block.inlines)}\n </Typography>\n )\n case 'image':\n return (\n <Box\n key={index}\n component=\"img\"\n // Resolved (AGL-1686), so the console preview shows the same\n // picture the published page will. No `hostId`: this renders\n // in the console, which has no site context — the scope the\n // picker baked in is what resolves. The console serves\n // `/api/media/cdn/…` itself, so the relative URL this\n // produces is fetchable from here.\n src={resolveMediaSrc(block.src)}\n alt={block.alt}\n loading=\"lazy\"\n sx={{\n maxWidth: '100%',\n borderRadius: 1,\n border: 1,\n borderColor: 'divider',\n }}\n />\n )\n case 'list':\n return (\n <Stack key={index} component=\"ul\" spacing={0.5} sx={{ my: 0 }}>\n {block.items.map((item, itemIndex) => (\n <Typography key={itemIndex} component=\"li\" variant=\"body2\">\n {renderInlines(item)}\n </Typography>\n ))}\n </Stack>\n )\n // A numbered list (AGL-1320) — a real `<ol>` with `start`, so the\n // console preview counts the same way the published page does.\n case 'orderedList':\n return (\n <Stack\n key={index}\n component=\"ol\"\n start={block.start}\n spacing={0.5}\n sx={{ my: 0 }}\n >\n {block.items.map((item, itemIndex) => (\n <Typography key={itemIndex} component=\"li\" variant=\"body2\">\n {renderInlines(item)}\n </Typography>\n ))}\n </Stack>\n )\n case 'code':\n return (\n <Box\n key={index}\n component=\"pre\"\n sx={{\n my: 0,\n p: 1.5,\n overflowX: 'auto',\n borderRadius: 1,\n border: 1,\n borderColor: 'divider',\n bgcolor: 'action.hover',\n fontFamily: 'monospace',\n fontSize: 13,\n }}\n >\n <code>{block.text}</code>\n </Box>\n )\n case 'table':\n return (\n <Box key={index} sx={{ overflowX: 'auto' }}>\n <Box\n component=\"table\"\n sx={{\n borderCollapse: 'collapse',\n width: '100%',\n '& th, & td': {\n border: 1,\n borderColor: 'divider',\n px: 1,\n py: 0.5,\n },\n '& th': { bgcolor: 'action.hover' },\n }}\n >\n <thead>\n <tr>\n {block.header.map((cell, cellIndex) => (\n <Typography\n key={cellIndex}\n component=\"th\"\n variant=\"body2\"\n sx={{\n textAlign: block.align[cellIndex] ?? 'left',\n fontWeight: 600,\n }}\n >\n {renderInlines(cell)}\n </Typography>\n ))}\n </tr>\n </thead>\n <tbody>\n {block.rows.map((row, rowIndex) => (\n <tr key={rowIndex}>\n {row.map((cell, cellIndex) => (\n <Typography\n key={cellIndex}\n component=\"td\"\n variant=\"body2\"\n sx={{ textAlign: block.align[cellIndex] ?? 'left' }}\n >\n {renderInlines(cell)}\n </Typography>\n ))}\n </tr>\n ))}\n </tbody>\n </Box>\n </Box>\n )\n // A quote (AGL-1315): italic behind a left accent, the muted\n // console idiom of the tenant's pull-quote.\n case 'quote':\n return (\n <Typography\n key={index}\n component=\"blockquote\"\n variant=\"body2\"\n sx={{\n my: 0,\n mx: 0,\n pl: 1.5,\n borderLeft: 3,\n borderColor: 'divider',\n fontStyle: 'italic',\n color: 'text.secondary',\n }}\n >\n {renderInlines(block.inlines)}\n </Typography>\n )\n default:\n return (\n <Typography key={index} variant=\"body2\">\n {renderInlines(block.inlines)}\n </Typography>\n )\n }\n })}\n </Stack>\n )\n}\n\nexport default MarkdownLiteView\n"],"names":["HEADING_ANCHOR_SCROLL_MARGIN","isMarkdownLinkReference","LinkTargetSearchContext","linkTargetKind","markdownHeadingSlugs","parseMarkdownLite","parseScreenLinkValue","resolveMediaSrc","useLinkTargetLabel","Box","Link","MuiLink","Stack","Typography","useContext","useMemo","REFERENCE_NOUNS","screen","collection","feed","entry","ReferenceLinkText","href","children","key","search","target","named","pending","label","available","component","data-md-reference","title","sx","color","textDecoration","renderInlines","inlines","map","inline","index","type","strong","text","em","rel","underline","span","MarkdownLiteView","source","blocks","slugs","spacing","block","id","variant","level","scrollMarginTop","src","alt","loading","maxWidth","borderRadius","border","borderColor","my","items","item","itemIndex","start","p","overflowX","bgcolor","fontFamily","fontSize","code","borderCollapse","width","px","py","thead","tr","header","cell","cellIndex","textAlign","align","fontWeight","tbody","rows","row","rowIndex","mx","pl","borderLeft","fontStyle"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAcA,SACEA,4BAA4B,EAC5BC,uBAAuB,EACvBC,uBAAuB,EACvBC,cAAc,EACdC,oBAAoB,EACpBC,iBAAiB,EACjBC,oBAAoB,EACpBC,eAAe,EACfC,kBAAkB,QACb,eAAc;AACrB,SAASC,GAAG,EAAEC,QAAQC,OAAO,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACvE,SAASC,UAAU,EAAEC,OAAO,QAAwB,QAAO;AAE3D,kEAAkE,GAClE,MAAMC,kBAAkE;IACtEC,QAAQ;IACRC,YAAY;IACZC,MAAM;IACNC,OAAO;AACT;AAEA;;;;;;;;CAQC,GACD,SAASC,kBAAkB,EACzBC,IAAI,EACJC,QAAQ,EAIT;IACC,MAAMC,MAAMlB,qBAAqBgB;IACjC,MAAMG,SAASX,WAAWZ;IAC1B,MAAMwB,SAASlB,mBAAmBgB;IAClC,0EAA0E;IAC1E,2EAA2E;IAC3E,MAAMG,QACJD,UACA,CAACA,OAAOE,OAAO,IACfF,OAAOG,KAAK,KAAKL,OAChBrB,CAAAA,eAAeqB,cAAAA,MAAOF,UAAU,WAAWG,OAAOK,SAAS,AAAD;IAC7D,qBACE,KAACrB;QACCsB,WAAU;QACVC,qBAAkB;QAClBC,OAAO,CAAC,SAAS,EACfN,QAAQD,OAAOG,KAAK,GAAGb,eAAe,CAACb,eAAeqB,cAAAA,MAAOF,MAAM,EACnE;QACFY,IAAI;YAAEC,OAAO;YAAgBC,gBAAgB;QAAmB;kBAE/Db;;AAGP;AAEA,MAAMc,gBAAgB,CAACC,UACrBA,QAAQC,GAAG,CAAC,CAACC,QAAQC,QACnBD,OAAOE,IAAI,KAAK,uBACd,KAACC;sBAAoBH,OAAOI,IAAI;WAAnBH,SACXD,OAAOE,IAAI,KAAK,yBAClB,KAACG;sBAAgBL,OAAOI,IAAI;WAAnBH,SACPD,OAAOE,IAAI,KAAK,SAClBzC,wBAAwBuC,OAAOlB,IAAI,kBACjC,KAACD;YAA8BC,MAAMkB,OAAOlB,IAAI;sBAC7CkB,OAAOI,IAAI;WADUH,uBAIxB,KAAC9B;YAECW,MAAMkB,OAAOlB,IAAI;YACjBI,QAAO;YACPoB,KAAI;YACJC,WAAU;sBAETP,OAAOI,IAAI;WANPH,uBAUT,KAACO;sBAAkBR,OAAOI,IAAI;WAAnBH;AASjB,OAAO,SAASQ,iBAAiB,EAAEC,MAAM,EAAyB;IAChE,MAAMC,SAASpC,QAAQ,IAAMV,kBAAkB6C,SAAS;QAACA;KAAO;IAChE,sEAAsE;IACtE,wEAAwE;IACxE,uEAAuE;IACvE,oBAAoB;IACpB,MAAME,QAAQrC,QAAQ,IAAMX,qBAAqB+C,SAAS;QAACA;KAAO;IAClE,qBACE,KAACvC;QAAMyC,SAAS;kBACbF,OAAOZ,GAAG,CAAC,CAACe,OAAOb;YAClB,OAAQa,MAAMZ,IAAI;gBAChB,KAAK;oBACH,qBACE,KAAC7B;wBAEC0C,IAAIH,KAAK,CAACX,MAAM;wBAChBe,SAASF,MAAMG,KAAK,KAAK,IAAI,OAAO;wBACpC1B,WAAWuB,MAAMG,KAAK,KAAK,IAAI,OAAO;wBACtCvB,IAAI;4BACFwB,iBAAiB,GAAG1D,6BAA6B,EAAE,CAAC;wBACtD;kCAECqC,cAAciB,MAAMhB,OAAO;uBARvBG;gBAWX,KAAK;oBACH,qBACE,KAAChC;wBAECsB,WAAU;wBACV,6DAA6D;wBAC7D,6DAA6D;wBAC7D,4DAA4D;wBAC5D,uDAAuD;wBACvD,sDAAsD;wBACtD,mCAAmC;wBACnC4B,KAAKpD,gBAAgB+C,MAAMK,GAAG;wBAC9BC,KAAKN,MAAMM,GAAG;wBACdC,SAAQ;wBACR3B,IAAI;4BACF4B,UAAU;4BACVC,cAAc;4BACdC,QAAQ;4BACRC,aAAa;wBACf;uBAhBKxB;gBAmBX,KAAK;oBACH,qBACE,KAAC7B;wBAAkBmB,WAAU;wBAAKsB,SAAS;wBAAKnB,IAAI;4BAAEgC,IAAI;wBAAE;kCACzDZ,MAAMa,KAAK,CAAC5B,GAAG,CAAC,CAAC6B,MAAMC,0BACtB,KAACxD;gCAA2BkB,WAAU;gCAAKyB,SAAQ;0CAChDnB,cAAc+B;+BADAC;uBAFT5B;gBAQhB,kEAAkE;gBAClE,+DAA+D;gBAC/D,KAAK;oBACH,qBACE,KAAC7B;wBAECmB,WAAU;wBACVuC,OAAOhB,MAAMgB,KAAK;wBAClBjB,SAAS;wBACTnB,IAAI;4BAAEgC,IAAI;wBAAE;kCAEXZ,MAAMa,KAAK,CAAC5B,GAAG,CAAC,CAAC6B,MAAMC,0BACtB,KAACxD;gCAA2BkB,WAAU;gCAAKyB,SAAQ;0CAChDnB,cAAc+B;+BADAC;uBAPd5B;gBAaX,KAAK;oBACH,qBACE,KAAChC;wBAECsB,WAAU;wBACVG,IAAI;4BACFgC,IAAI;4BACJK,GAAG;4BACHC,WAAW;4BACXT,cAAc;4BACdC,QAAQ;4BACRC,aAAa;4BACbQ,SAAS;4BACTC,YAAY;4BACZC,UAAU;wBACZ;kCAEA,cAAA,KAACC;sCAAMtB,MAAMV,IAAI;;uBAdZH;gBAiBX,KAAK;oBACH,qBACE,KAAChC;wBAAgByB,IAAI;4BAAEsC,WAAW;wBAAO;kCACvC,cAAA,MAAC/D;4BACCsB,WAAU;4BACVG,IAAI;gCACF2C,gBAAgB;gCAChBC,OAAO;gCACP,cAAc;oCACZd,QAAQ;oCACRC,aAAa;oCACbc,IAAI;oCACJC,IAAI;gCACN;gCACA,QAAQ;oCAAEP,SAAS;gCAAe;4BACpC;;8CAEA,KAACQ;8CACC,cAAA,KAACC;kDACE5B,MAAM6B,MAAM,CAAC5C,GAAG,CAAC,CAAC6C,MAAMC;gDAMR/B;iEALf,KAACzC;gDAECkB,WAAU;gDACVyB,SAAQ;gDACRtB,IAAI;oDACFoD,SAAS,GAAEhC,yBAAAA,MAAMiC,KAAK,CAACF,UAAU,YAAtB/B,yBAA0B;oDACrCkC,YAAY;gDACd;0DAECnD,cAAc+C;+CARVC;;;;8CAab,KAACI;8CACEnC,MAAMoC,IAAI,CAACnD,GAAG,CAAC,CAACoD,KAAKC,yBACpB,KAACV;sDACES,IAAIpD,GAAG,CAAC,CAAC6C,MAAMC;oDAKK/B;qEAJnB,KAACzC;oDAECkB,WAAU;oDACVyB,SAAQ;oDACRtB,IAAI;wDAAEoD,SAAS,GAAEhC,yBAAAA,MAAMiC,KAAK,CAACF,UAAU,YAAtB/B,yBAA0B;oDAAO;8DAEjDjB,cAAc+C;mDALVC;;2CAHFO;;;;uBAlCPnD;gBAmDd,6DAA6D;gBAC7D,4CAA4C;gBAC5C,KAAK;oBACH,qBACE,KAAC5B;wBAECkB,WAAU;wBACVyB,SAAQ;wBACRtB,IAAI;4BACFgC,IAAI;4BACJ2B,IAAI;4BACJC,IAAI;4BACJC,YAAY;4BACZ9B,aAAa;4BACb+B,WAAW;4BACX7D,OAAO;wBACT;kCAECE,cAAciB,MAAMhB,OAAO;uBAbvBG;gBAgBX;oBACE,qBACE,KAAC5B;wBAAuB2C,SAAQ;kCAC7BnB,cAAciB,MAAMhB,OAAO;uBADbG;YAIvB;QACF;;AAGN;AAEA,eAAeQ,iBAAgB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* The toolbar's other half (AGL-985): the same commands applied to a raw
|
|
19
|
+
* markdown textarea instead of the visual editor's block model. One
|
|
20
|
+
* implementation, because the blog body and the marketplace README are the
|
|
21
|
+
* same dialect behind the same toolbar.
|
|
22
|
+
*/
|
|
23
|
+
import type { MarkdownEditorCommand } from './markdown-visual-editor.component';
|
|
24
|
+
export interface SourceEdit {
|
|
25
|
+
body: string;
|
|
26
|
+
/** Selection to restore — the inserted run, so it can be typed over. */
|
|
27
|
+
start: number;
|
|
28
|
+
end: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Wraps or inserts at [start, end) of `body`. Block-level constructs get a
|
|
32
|
+
* blank line ahead of them, or markdown-lite would fold them into the
|
|
33
|
+
* paragraph above.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyCommandToSource(body: string, start: number, end: number, command: MarkdownEditorCommand): SourceEdit;
|
|
36
|
+
/**
|
|
37
|
+
* Writes one link over [start, end) — what the link dialog's answer becomes
|
|
38
|
+
* on the raw-source surface (AGL-3119).
|
|
39
|
+
*
|
|
40
|
+
* Separate from the command above because a link is the one command that
|
|
41
|
+
* cannot be a template: `[link text](https://)` is a placeholder the author
|
|
42
|
+
* has to finish by hand, and the whole point of the dialog is that an
|
|
43
|
+
* internal target is PICKED, so what lands here is already a reference or a
|
|
44
|
+
* checked address.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyLinkToSource(body: string, start: number, end: number, link: {
|
|
47
|
+
href: string;
|
|
48
|
+
text?: string;
|
|
49
|
+
}): SourceEdit;
|
|
50
|
+
/** Markdown-lite's syntax, as one line of helper text under a source box. */
|
|
51
|
+
export declare const MARKDOWN_SOURCE_HINT: string;
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
* The toolbar's other half (AGL-985): the same commands applied to a raw
|
|
18
|
+
* markdown textarea instead of the visual editor's block model. One
|
|
19
|
+
* implementation, because the blog body and the marketplace README are the
|
|
20
|
+
* same dialect behind the same toolbar.
|
|
21
|
+
*/ /**
|
|
22
|
+
* Wraps or inserts at [start, end) of `body`. Block-level constructs get a
|
|
23
|
+
* blank line ahead of them, or markdown-lite would fold them into the
|
|
24
|
+
* paragraph above.
|
|
25
|
+
*/ export function applyCommandToSource(body, start, end, command) {
|
|
26
|
+
const selected = body.slice(start, end);
|
|
27
|
+
const blockPrefix = start > 0 && !/\n\n$/.test(body.slice(0, start)) ? '\n\n' : '';
|
|
28
|
+
const insert = command === 'bold' ? `**${selected || 'bold text'}**` : command === 'italic' ? `*${selected || 'italic text'}*` : command === 'heading' ? `${blockPrefix}## ${selected || 'Heading'}` : command === 'heading3' ? `${blockPrefix}### ${selected || 'Heading'}` : command === 'list' ? `${blockPrefix}- ${selected || 'list item'}` : command === 'orderedList' ? `${blockPrefix}1. ${selected || 'list item'}` : command === 'quote' ? `${blockPrefix}> ${selected || 'quote'}` : command === 'link' ? `[${selected || 'link text'}](https://)` : command === 'code' ? `${blockPrefix}\`\`\`\n${selected || 'code'}\n\`\`\`` : command === 'table' ? `${blockPrefix}| Column | Column |\n| --- | --- |\n| | |` : `${blockPrefix}`;
|
|
29
|
+
return {
|
|
30
|
+
body: body.slice(0, start) + insert + body.slice(end),
|
|
31
|
+
start,
|
|
32
|
+
end: start + insert.length
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Writes one link over [start, end) — what the link dialog's answer becomes
|
|
37
|
+
* on the raw-source surface (AGL-3119).
|
|
38
|
+
*
|
|
39
|
+
* Separate from the command above because a link is the one command that
|
|
40
|
+
* cannot be a template: `[link text](https://)` is a placeholder the author
|
|
41
|
+
* has to finish by hand, and the whole point of the dialog is that an
|
|
42
|
+
* internal target is PICKED, so what lands here is already a reference or a
|
|
43
|
+
* checked address.
|
|
44
|
+
*/ export function applyLinkToSource(body, start, end, link) {
|
|
45
|
+
var _link_text;
|
|
46
|
+
const text = ((_link_text = link.text) == null ? void 0 : _link_text.trim()) || body.slice(start, end).trim() || link.href;
|
|
47
|
+
const insert = `[${text}](${link.href})`;
|
|
48
|
+
return {
|
|
49
|
+
body: body.slice(0, start) + insert + body.slice(end),
|
|
50
|
+
start,
|
|
51
|
+
end: start + insert.length
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Markdown-lite's syntax, as one line of helper text under a source box. */ export const MARKDOWN_SOURCE_HINT = 'Markdown-lite: **bold**, *italic*, # or ## headings, - lists, ' + '1. numbered lists, [links](https:// or /page), , ' + '``` code fences, | pipe | tables |, > quotes.';
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=markdown-source-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../libs/aglyn-markdown-editor/src/lib/markdown-source-command.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 * The toolbar's other half (AGL-985): the same commands applied to a raw\n * markdown textarea instead of the visual editor's block model. One\n * implementation, because the blog body and the marketplace README are the\n * same dialect behind the same toolbar.\n */\n\nimport type { MarkdownEditorCommand } from './markdown-visual-editor.component'\n\nexport interface SourceEdit {\n body: string\n /** Selection to restore — the inserted run, so it can be typed over. */\n start: number\n end: number\n}\n\n/**\n * Wraps or inserts at [start, end) of `body`. Block-level constructs get a\n * blank line ahead of them, or markdown-lite would fold them into the\n * paragraph above.\n */\nexport function applyCommandToSource(\n body: string,\n start: number,\n end: number,\n command: MarkdownEditorCommand,\n): SourceEdit {\n const selected = body.slice(start, end)\n const blockPrefix =\n start > 0 && !/\\n\\n$/.test(body.slice(0, start)) ? '\\n\\n' : ''\n const insert =\n command === 'bold'\n ? `**${selected || 'bold text'}**`\n : command === 'italic'\n ? `*${selected || 'italic text'}*`\n : command === 'heading'\n ? `${blockPrefix}## ${selected || 'Heading'}`\n : command === 'heading3'\n ? `${blockPrefix}### ${selected || 'Heading'}`\n : command === 'list'\n ? `${blockPrefix}- ${selected || 'list item'}`\n : command === 'orderedList'\n ? `${blockPrefix}1. ${selected || 'list item'}`\n : command === 'quote'\n ? `${blockPrefix}> ${selected || 'quote'}`\n : command === 'link'\n ? `[${selected || 'link text'}](https://)`\n : command === 'code'\n ? `${blockPrefix}\\`\\`\\`\\n${selected || 'code'}\\n\\`\\`\\``\n : command === 'table'\n ? `${blockPrefix}| Column | Column |\\n| --- | --- |\\n| | |`\n : `${blockPrefix}`\n return {\n body: body.slice(0, start) + insert + body.slice(end),\n start,\n end: start + insert.length,\n }\n}\n\n/**\n * Writes one link over [start, end) — what the link dialog's answer becomes\n * on the raw-source surface (AGL-3119).\n *\n * Separate from the command above because a link is the one command that\n * cannot be a template: `[link text](https://)` is a placeholder the author\n * has to finish by hand, and the whole point of the dialog is that an\n * internal target is PICKED, so what lands here is already a reference or a\n * checked address.\n */\nexport function applyLinkToSource(\n body: string,\n start: number,\n end: number,\n link: { href: string; text?: string },\n): SourceEdit {\n const text = link.text?.trim() || body.slice(start, end).trim() || link.href\n const insert = `[${text}](${link.href})`\n return {\n body: body.slice(0, start) + insert + body.slice(end),\n start,\n end: start + insert.length,\n }\n}\n\n/** Markdown-lite's syntax, as one line of helper text under a source box. */\nexport const MARKDOWN_SOURCE_HINT =\n 'Markdown-lite: **bold**, *italic*, # or ## headings, - lists, ' +\n '1. numbered lists, [links](https:// or /page), , ' +\n '``` code fences, | pipe | tables |, > quotes.'\n"],"names":["applyCommandToSource","body","start","end","command","selected","slice","blockPrefix","test","insert","length","applyLinkToSource","link","text","trim","href","MARKDOWN_SOURCE_HINT"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;CAKC,GAWD;;;;CAIC,GACD,OAAO,SAASA,qBACdC,IAAY,EACZC,KAAa,EACbC,GAAW,EACXC,OAA8B;IAE9B,MAAMC,WAAWJ,KAAKK,KAAK,CAACJ,OAAOC;IACnC,MAAMI,cACJL,QAAQ,KAAK,CAAC,QAAQM,IAAI,CAACP,KAAKK,KAAK,CAAC,GAAGJ,UAAU,SAAS;IAC9D,MAAMO,SACJL,YAAY,SACR,CAAC,EAAE,EAAEC,YAAY,YAAY,EAAE,CAAC,GAChCD,YAAY,WACV,CAAC,CAAC,EAAEC,YAAY,cAAc,CAAC,CAAC,GAChCD,YAAY,YACV,GAAGG,YAAY,GAAG,EAAEF,YAAY,WAAW,GAC3CD,YAAY,aACV,GAAGG,YAAY,IAAI,EAAEF,YAAY,WAAW,GAC5CD,YAAY,SACV,GAAGG,YAAY,EAAE,EAAEF,YAAY,aAAa,GAC5CD,YAAY,gBACZ,GAAGG,YAAY,GAAG,EAAEF,YAAY,aAAa,GAC7CD,YAAY,UACZ,GAAGG,YAAY,EAAE,EAAEF,YAAY,SAAS,GACxCD,YAAY,SACV,CAAC,CAAC,EAAEC,YAAY,YAAY,WAAW,CAAC,GACxCD,YAAY,SACV,GAAGG,YAAY,QAAQ,EAAEF,YAAY,OAAO,QAAQ,CAAC,GACrDD,YAAY,UACV,GAAGG,YAAY,2CAA2C,CAAC,GAC3D,GAAGA,YAAY,EAAE,EAAEF,YAAY,WAAW,WAAW,CAAC;IAC1E,OAAO;QACLJ,MAAMA,KAAKK,KAAK,CAAC,GAAGJ,SAASO,SAASR,KAAKK,KAAK,CAACH;QACjDD;QACAC,KAAKD,QAAQO,OAAOC,MAAM;IAC5B;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,kBACdV,IAAY,EACZC,KAAa,EACbC,GAAW,EACXS,IAAqC;QAExBA;IAAb,MAAMC,OAAOD,EAAAA,aAAAA,KAAKC,IAAI,qBAATD,WAAWE,IAAI,OAAMb,KAAKK,KAAK,CAACJ,OAAOC,KAAKW,IAAI,MAAMF,KAAKG,IAAI;IAC5E,MAAMN,SAAS,CAAC,CAAC,EAAEI,KAAK,EAAE,EAAED,KAAKG,IAAI,CAAC,CAAC,CAAC;IACxC,OAAO;QACLd,MAAMA,KAAKK,KAAK,CAAC,GAAGJ,SAASO,SAASR,KAAKK,KAAK,CAACH;QACjDD;QACAC,KAAKD,QAAQO,OAAOC,MAAM;IAC5B;AACF;AAEA,2EAA2E,GAC3E,OAAO,MAAMM,uBACX,mEACA,yEACA,gDAA+C"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WYSIWYG editor for markdown-lite blog bodies (AGL-582).
|
|
3
|
+
*
|
|
4
|
+
* Model-first, dependency-free: the parsed markdown-lite blocks are the
|
|
5
|
+
* source of truth (`rowsRef`), rendered as ONE contentEditable element per
|
|
6
|
+
* text row (paragraph / heading / list item) plus non-editable image rows.
|
|
7
|
+
* Plain typing stays native — each `input` event reads the row's DOM back
|
|
8
|
+
* into the model WITHOUT re-rendering, so the caret and IME composition are
|
|
9
|
+
* never disturbed. Structural edits (Enter, Backspace-at-start, toolbar
|
|
10
|
+
* marks, links, images, paste, undo) mutate the model, bump `version` to
|
|
11
|
+
* re-render every row from the model, and restore the selection from model
|
|
12
|
+
* offsets. Every change serializes through `serializeMarkdownLite`, so the
|
|
13
|
+
* document round-trips losslessly to the exact markdown-lite string stored
|
|
14
|
+
* in Firestore — no HTML is ever persisted.
|
|
15
|
+
*/
|
|
16
|
+
import type * as Aglyn from '@aglyn/aglyn';
|
|
17
|
+
export type MarkdownEditorCommand = 'bold' | 'italic' | 'heading' | 'heading3' | 'list' | 'orderedList' | 'link' | 'image' | 'code' | 'table' | 'quote';
|
|
18
|
+
/**
|
|
19
|
+
* What the caret is sitting in (AGL-984), so a toolbar can show its state.
|
|
20
|
+
* `kind` is the caret row's kind, or null when the caret is in a block row
|
|
21
|
+
* (a table cell or a snippet), where the mark commands do not apply.
|
|
22
|
+
*/
|
|
23
|
+
export interface MarkdownEditorContext {
|
|
24
|
+
bold: boolean;
|
|
25
|
+
italic: boolean;
|
|
26
|
+
kind: 'paragraph' | 'heading2' | 'heading3' | 'listItem' | 'orderedItem' | 'quote' | null;
|
|
27
|
+
}
|
|
28
|
+
export interface MarkdownVisualEditorHandle {
|
|
29
|
+
/** Applies a toolbar command at the current (or last known) selection. */
|
|
30
|
+
exec: (command: MarkdownEditorCommand) => void;
|
|
31
|
+
/** Inserts an image block after the caret row (console media picker). */
|
|
32
|
+
insertImage: (alt: string, url: string) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface MarkdownVisualEditorProps {
|
|
35
|
+
/** Markdown-lite source. Re-parsed whenever it changes externally. */
|
|
36
|
+
value: string;
|
|
37
|
+
/** Fires with the serialized markdown-lite string on every edit. */
|
|
38
|
+
onChange: (markdown: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* When set, the Insert image dialog offers a "Choose from media" button
|
|
41
|
+
* that closes the dialog and invokes this callback — the host page opens
|
|
42
|
+
* its media picker and inserts via the imperative handle (AGL-596).
|
|
43
|
+
*
|
|
44
|
+
* Receives the alt text typed in the dialog before the handoff (AGL-1645).
|
|
45
|
+
* Handing the picker an image is the only chance to describe it: an image
|
|
46
|
+
* row is `contentEditable={false}`, so an alt left empty here can never be
|
|
47
|
+
* filled in afterwards without deleting and re-inserting the image. Callers
|
|
48
|
+
* that mint their own alt from the asset are free to ignore it.
|
|
49
|
+
*/
|
|
50
|
+
onPickImageFromMedia?: (alt: string) => void;
|
|
51
|
+
/** Fires when the caret's formatting context changes (AGL-984). */
|
|
52
|
+
onContextChange?: (context: MarkdownEditorContext) => void;
|
|
53
|
+
minHeight?: number | string;
|
|
54
|
+
maxHeight?: number | string;
|
|
55
|
+
}
|
|
56
|
+
type TextRowKind = 'paragraph' | 'heading2' | 'heading3' | 'listItem' | 'orderedItem' | 'quote';
|
|
57
|
+
type TextRow = {
|
|
58
|
+
key: string;
|
|
59
|
+
kind: TextRowKind;
|
|
60
|
+
inlines: Aglyn.MarkdownInline[];
|
|
61
|
+
/**
|
|
62
|
+
* An ordered list's start number, carried on the row that OPENS the run
|
|
63
|
+
* (AGL-1320). The row model is flat, so this is the only place a non-1
|
|
64
|
+
* start can live — without it, editing a notice that resumes at `7.` would
|
|
65
|
+
* silently renumber it to `1.` on the next save, the same way an
|
|
66
|
+
* unrepresentable block used to be dropped. A row that does not open a run
|
|
67
|
+
* ignores it; if the opener is deleted, the run restarts at 1.
|
|
68
|
+
*/
|
|
69
|
+
start?: number;
|
|
70
|
+
};
|
|
71
|
+
type ImageRow = {
|
|
72
|
+
key: string;
|
|
73
|
+
kind: 'image';
|
|
74
|
+
src: string;
|
|
75
|
+
alt: string;
|
|
76
|
+
};
|
|
77
|
+
type CodeRow = {
|
|
78
|
+
key: string;
|
|
79
|
+
kind: 'code';
|
|
80
|
+
lang: string;
|
|
81
|
+
text: string;
|
|
82
|
+
};
|
|
83
|
+
type TableRow = {
|
|
84
|
+
key: string;
|
|
85
|
+
kind: 'table';
|
|
86
|
+
align: Aglyn.MarkdownTableAlign[];
|
|
87
|
+
header: Aglyn.MarkdownTableCell[];
|
|
88
|
+
rows: Aglyn.MarkdownTableCell[][];
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Rows the caret cannot enter: they render read-only with a Remove button
|
|
92
|
+
* and are carried through the model untouched. Code blocks and tables join
|
|
93
|
+
* images here (AGL-974) — this editor also writes marketplace READMEs, and
|
|
94
|
+
* a block it could not represent was silently DELETED on the next save.
|
|
95
|
+
*/
|
|
96
|
+
type BlockRow = ImageRow | CodeRow | TableRow;
|
|
97
|
+
export type EditorRow = TextRow | BlockRow;
|
|
98
|
+
/** Flattens parsed blocks to editable rows (lists become item rows). */
|
|
99
|
+
export declare function markdownToRows(markdown: string): EditorRow[];
|
|
100
|
+
/** Regroups rows to blocks (adjacent list items rejoin) and serializes. */
|
|
101
|
+
export declare function rowsToMarkdown(rows: EditorRow[]): string;
|
|
102
|
+
/**
|
|
103
|
+
* Reads a row's live DOM back into canonical inlines. Only elements this
|
|
104
|
+
* editor renders exist in a row (plus text nodes the browser adds while
|
|
105
|
+
* typing); unknown wrappers are recursed through as plain text.
|
|
106
|
+
*/
|
|
107
|
+
export declare function readInlinesFromElement(rowEl: HTMLElement): Aglyn.MarkdownInline[];
|
|
108
|
+
declare const MarkdownVisualEditor: import("react").ForwardRefExoticComponent<MarkdownVisualEditorProps & import("react").RefAttributes<MarkdownVisualEditorHandle>>;
|
|
109
|
+
export default MarkdownVisualEditor;
|