@wingleeio/mugen-markdown 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +158 -12
- package/dist/index.d.cts +45 -3
- package/dist/index.d.mts +45 -3
- package/dist/index.mjs +159 -13
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -162,7 +162,16 @@ const defaultTheme = {
|
|
|
162
162
|
background: "rgba(127, 127, 127, 0.12)",
|
|
163
163
|
color: "inherit",
|
|
164
164
|
radius: 8,
|
|
165
|
-
highlight: defaultTokenColors
|
|
165
|
+
highlight: defaultTokenColors,
|
|
166
|
+
header: {
|
|
167
|
+
show: false,
|
|
168
|
+
height: 38,
|
|
169
|
+
fontSize: 12,
|
|
170
|
+
background: "rgba(127, 127, 127, 0.06)",
|
|
171
|
+
color: "rgba(127, 127, 127, 0.85)",
|
|
172
|
+
borderColor: "rgba(127, 127, 127, 0.18)",
|
|
173
|
+
buttonBackground: "rgba(127, 127, 127, 0.04)"
|
|
174
|
+
}
|
|
166
175
|
},
|
|
167
176
|
blockquote: {
|
|
168
177
|
padding: 14,
|
|
@@ -1311,7 +1320,7 @@ function lineCount(value) {
|
|
|
1311
1320
|
function measureCodeBlock(props, ctx) {
|
|
1312
1321
|
(0, _wingleeio_mugen.assertMeasurableFont)(props.font);
|
|
1313
1322
|
const pad = props.padding ?? 0;
|
|
1314
|
-
return lineCount(props.value) * props.lineHeight + 2 * pad;
|
|
1323
|
+
return (props.header ? props.header.height : 0) + lineCount(props.value) * props.lineHeight + 2 * pad;
|
|
1315
1324
|
}
|
|
1316
1325
|
const colorsCache = /* @__PURE__ */ new WeakMap();
|
|
1317
1326
|
function resolveTokenColors(overrides) {
|
|
@@ -1375,9 +1384,124 @@ function HighlightedCode(props) {
|
|
|
1375
1384
|
style: overlayStyle
|
|
1376
1385
|
}));
|
|
1377
1386
|
}
|
|
1387
|
+
function legacyCopy(text) {
|
|
1388
|
+
if (typeof document === "undefined") return false;
|
|
1389
|
+
const ta = document.createElement("textarea");
|
|
1390
|
+
ta.value = text;
|
|
1391
|
+
ta.setAttribute("readonly", "");
|
|
1392
|
+
ta.style.position = "fixed";
|
|
1393
|
+
ta.style.top = "0";
|
|
1394
|
+
ta.style.left = "0";
|
|
1395
|
+
ta.style.opacity = "0";
|
|
1396
|
+
ta.style.pointerEvents = "none";
|
|
1397
|
+
document.body.appendChild(ta);
|
|
1398
|
+
ta.select();
|
|
1399
|
+
let ok = false;
|
|
1400
|
+
try {
|
|
1401
|
+
ok = document.execCommand("copy");
|
|
1402
|
+
} catch {
|
|
1403
|
+
ok = false;
|
|
1404
|
+
}
|
|
1405
|
+
document.body.removeChild(ta);
|
|
1406
|
+
return ok;
|
|
1407
|
+
}
|
|
1408
|
+
async function copyText(text) {
|
|
1409
|
+
const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
1410
|
+
if (clip?.writeText != null) try {
|
|
1411
|
+
await clip.writeText(text);
|
|
1412
|
+
return true;
|
|
1413
|
+
} catch {}
|
|
1414
|
+
return legacyCopy(text);
|
|
1415
|
+
}
|
|
1416
|
+
/** Copies the block's raw text; flips to "Copied" for ~1.6s on success. */
|
|
1417
|
+
function CopyButton(props) {
|
|
1418
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
1419
|
+
const [hovered, setHovered] = (0, react.useState)(false);
|
|
1420
|
+
const timer = (0, react.useRef)(null);
|
|
1421
|
+
(0, react.useEffect)(() => () => {
|
|
1422
|
+
if (timer.current != null) clearTimeout(timer.current);
|
|
1423
|
+
}, []);
|
|
1424
|
+
const onClick = () => {
|
|
1425
|
+
copyText(props.value).then((ok) => {
|
|
1426
|
+
if (!ok) return;
|
|
1427
|
+
setCopied(true);
|
|
1428
|
+
if (timer.current != null) clearTimeout(timer.current);
|
|
1429
|
+
timer.current = setTimeout(() => setCopied(false), 1600);
|
|
1430
|
+
}).catch(() => {});
|
|
1431
|
+
};
|
|
1432
|
+
const style = {
|
|
1433
|
+
display: "inline-flex",
|
|
1434
|
+
flex: "0 0 auto",
|
|
1435
|
+
alignItems: "center",
|
|
1436
|
+
justifyContent: "center",
|
|
1437
|
+
whiteSpace: "nowrap",
|
|
1438
|
+
cursor: "pointer",
|
|
1439
|
+
borderRadius: 8,
|
|
1440
|
+
border: `1px solid ${props.borderColor ?? "rgba(127, 127, 127, 0.2)"}`,
|
|
1441
|
+
...props.background != null ? { background: props.background } : null,
|
|
1442
|
+
...props.color != null ? { color: props.color } : null,
|
|
1443
|
+
padding: "4px 9px",
|
|
1444
|
+
minWidth: "4.6em",
|
|
1445
|
+
fontFamily: "inherit",
|
|
1446
|
+
fontSize: `${props.fontSize}px`,
|
|
1447
|
+
lineHeight: 1,
|
|
1448
|
+
opacity: hovered || copied ? 1 : .82,
|
|
1449
|
+
transition: "opacity 120ms ease"
|
|
1450
|
+
};
|
|
1451
|
+
return (0, react.createElement)("button", {
|
|
1452
|
+
type: "button",
|
|
1453
|
+
onClick,
|
|
1454
|
+
onMouseEnter: () => setHovered(true),
|
|
1455
|
+
onMouseLeave: () => setHovered(false),
|
|
1456
|
+
"aria-label": copied ? "Copied" : "Copy code",
|
|
1457
|
+
style
|
|
1458
|
+
}, copied ? "Copied" : "Copy");
|
|
1459
|
+
}
|
|
1460
|
+
/** The fixed-height chrome bar: language label left, copy button right. */
|
|
1461
|
+
function CodeHeader(props) {
|
|
1462
|
+
const barStyle = {
|
|
1463
|
+
display: "flex",
|
|
1464
|
+
flex: "0 0 auto",
|
|
1465
|
+
alignItems: "center",
|
|
1466
|
+
justifyContent: "space-between",
|
|
1467
|
+
gap: 8,
|
|
1468
|
+
height: `${props.height}px`,
|
|
1469
|
+
boxSizing: "border-box",
|
|
1470
|
+
padding: "0 12px",
|
|
1471
|
+
fontFamily: props.fontFamily,
|
|
1472
|
+
...props.radius != null ? {
|
|
1473
|
+
borderTopLeftRadius: `${props.radius}px`,
|
|
1474
|
+
borderTopRightRadius: `${props.radius}px`
|
|
1475
|
+
} : null,
|
|
1476
|
+
...props.background != null ? { background: props.background } : null,
|
|
1477
|
+
...props.borderColor != null ? { borderBottom: `1px solid ${props.borderColor}` } : null
|
|
1478
|
+
};
|
|
1479
|
+
const labelStyle = {
|
|
1480
|
+
minWidth: 0,
|
|
1481
|
+
overflow: "hidden",
|
|
1482
|
+
textOverflow: "ellipsis",
|
|
1483
|
+
whiteSpace: "nowrap",
|
|
1484
|
+
fontSize: `${props.fontSize}px`,
|
|
1485
|
+
letterSpacing: "0.02em",
|
|
1486
|
+
fontVariantLigatures: "none",
|
|
1487
|
+
...props.color != null ? { color: props.color } : null
|
|
1488
|
+
};
|
|
1489
|
+
return (0, react.createElement)("div", { style: barStyle }, (0, react.createElement)("span", {
|
|
1490
|
+
key: "lang",
|
|
1491
|
+
style: labelStyle
|
|
1492
|
+
}, props.label), (0, react.createElement)(CopyButton, {
|
|
1493
|
+
key: "copy",
|
|
1494
|
+
value: props.value,
|
|
1495
|
+
fontSize: props.fontSize,
|
|
1496
|
+
color: props.color,
|
|
1497
|
+
borderColor: props.borderColor,
|
|
1498
|
+
background: props.buttonBackground
|
|
1499
|
+
}));
|
|
1500
|
+
}
|
|
1378
1501
|
function renderCodeBlock(props) {
|
|
1379
1502
|
const pad = props.padding ?? 0;
|
|
1380
1503
|
const profile = props.highlight === false ? null : profileFor(props.lang);
|
|
1504
|
+
const header = props.header;
|
|
1381
1505
|
const preStyle = {
|
|
1382
1506
|
margin: 0,
|
|
1383
1507
|
padding: `${pad}px`,
|
|
@@ -1386,7 +1510,10 @@ function renderCodeBlock(props) {
|
|
|
1386
1510
|
boxSizing: "border-box",
|
|
1387
1511
|
...props.background != null ? { background: props.background } : null,
|
|
1388
1512
|
...props.color != null ? { color: props.color } : null,
|
|
1389
|
-
...props.radius != null ? { borderRadius: `${props.radius}px` } :
|
|
1513
|
+
...props.radius != null ? header == null ? { borderRadius: `${props.radius}px` } : {
|
|
1514
|
+
borderBottomLeftRadius: `${props.radius}px`,
|
|
1515
|
+
borderBottomRightRadius: `${props.radius}px`
|
|
1516
|
+
} : null,
|
|
1390
1517
|
...profile != null ? {
|
|
1391
1518
|
position: "relative",
|
|
1392
1519
|
tabSize: 8
|
|
@@ -1399,17 +1526,13 @@ function renderCodeBlock(props) {
|
|
|
1399
1526
|
margin: 0,
|
|
1400
1527
|
padding: 0
|
|
1401
1528
|
};
|
|
1402
|
-
|
|
1403
|
-
className: props.className,
|
|
1529
|
+
const pre = (0, react.createElement)("pre", {
|
|
1530
|
+
...header == null ? { className: props.className } : null,
|
|
1404
1531
|
style: preStyle
|
|
1405
|
-
}, (0, react.createElement)("code", {
|
|
1532
|
+
}, profile == null ? (0, react.createElement)("code", {
|
|
1406
1533
|
style: codeStyle,
|
|
1407
1534
|
...props.lang ? { "data-lang": props.lang } : null
|
|
1408
|
-
}, props.value))
|
|
1409
|
-
return (0, react.createElement)("pre", {
|
|
1410
|
-
className: props.className,
|
|
1411
|
-
style: preStyle
|
|
1412
|
-
}, (0, react.createElement)(HighlightedCode, {
|
|
1535
|
+
}, props.value) : (0, react.createElement)(HighlightedCode, {
|
|
1413
1536
|
value: props.value,
|
|
1414
1537
|
lang: props.lang,
|
|
1415
1538
|
font: props.font,
|
|
@@ -1419,6 +1542,20 @@ function renderCodeBlock(props) {
|
|
|
1419
1542
|
colors: resolveTokenColors(props.highlight === false ? void 0 : props.highlight),
|
|
1420
1543
|
codeStyle
|
|
1421
1544
|
}));
|
|
1545
|
+
if (header == null) return pre;
|
|
1546
|
+
return (0, react.createElement)("div", { className: props.className }, (0, react.createElement)(CodeHeader, {
|
|
1547
|
+
key: "header",
|
|
1548
|
+
label: header.label ?? props.lang ?? "code",
|
|
1549
|
+
value: props.value,
|
|
1550
|
+
height: header.height,
|
|
1551
|
+
fontSize: header.fontSize,
|
|
1552
|
+
fontFamily: header.fontFamily ?? "monospace",
|
|
1553
|
+
...props.radius != null ? { radius: props.radius } : null,
|
|
1554
|
+
background: header.background,
|
|
1555
|
+
color: header.color,
|
|
1556
|
+
borderColor: header.borderColor,
|
|
1557
|
+
buttonBackground: header.buttonBackground
|
|
1558
|
+
}), pre);
|
|
1422
1559
|
}
|
|
1423
1560
|
/** A measurable fenced-code primitive (no wrapping; height from line count). */
|
|
1424
1561
|
const CodeBlock = (0, _wingleeio_mugen.markPrimitive)(renderCodeBlock, {
|
|
@@ -1632,7 +1769,16 @@ const defaultComponents = {
|
|
|
1632
1769
|
background: c.background,
|
|
1633
1770
|
radius: c.radius,
|
|
1634
1771
|
highlight: c.highlight,
|
|
1635
|
-
...c.color !== "inherit" ? { color: c.color } : null
|
|
1772
|
+
...c.color !== "inherit" ? { color: c.color } : null,
|
|
1773
|
+
...c.header.show ? { header: {
|
|
1774
|
+
height: c.header.height,
|
|
1775
|
+
fontSize: c.header.fontSize,
|
|
1776
|
+
fontFamily: ctx.theme.monoFamily,
|
|
1777
|
+
background: c.header.background,
|
|
1778
|
+
color: c.header.color,
|
|
1779
|
+
borderColor: c.header.borderColor,
|
|
1780
|
+
buttonBackground: c.header.buttonBackground
|
|
1781
|
+
} } : null
|
|
1636
1782
|
});
|
|
1637
1783
|
},
|
|
1638
1784
|
list: ({ node, ctx }) => renderList(node, ctx),
|
package/dist/index.d.cts
CHANGED
|
@@ -110,6 +110,21 @@ interface MarkdownTheme {
|
|
|
110
110
|
* change a block's measured height.
|
|
111
111
|
*/
|
|
112
112
|
highlight: CodeTokenColors | false;
|
|
113
|
+
/**
|
|
114
|
+
* Optional chrome bar above the code — the language on the left, a
|
|
115
|
+
* copy-to-clipboard button on the right. `show: false` (the default) keeps
|
|
116
|
+
* the bare `<pre>`. When shown, the bar's fixed `height` is folded into the
|
|
117
|
+
* block's measured height, so computed and painted heights stay identical.
|
|
118
|
+
*/
|
|
119
|
+
header: {
|
|
120
|
+
/** Render the chrome bar. Off by default. */show: boolean; /** Fixed bar height in px (counted in the measured height). */
|
|
121
|
+
height: number; /** Label + button font size in px. */
|
|
122
|
+
fontSize: number; /** Bar background. */
|
|
123
|
+
background: string; /** Label + button text colour. */
|
|
124
|
+
color: string; /** Bottom hairline + button border colour. */
|
|
125
|
+
borderColor: string; /** Copy-button fill. */
|
|
126
|
+
buttonBackground: string;
|
|
127
|
+
};
|
|
113
128
|
};
|
|
114
129
|
blockquote: {
|
|
115
130
|
padding: number;
|
|
@@ -383,8 +398,9 @@ declare namespace Markdown {
|
|
|
383
398
|
//#region src/primitives/code-block.d.ts
|
|
384
399
|
/**
|
|
385
400
|
* A fenced code block. Code does not wrap — long lines scroll horizontally — so
|
|
386
|
-
* its height is simply `lineCount × lineHeight + 2 × padding
|
|
387
|
-
*
|
|
401
|
+
* its height is simply `lineCount × lineHeight + 2 × padding` (plus an optional
|
|
402
|
+
* fixed-height {@link CodeBlockHeader} bar), independent of the row width. That
|
|
403
|
+
* makes it trivially and exactly measurable.
|
|
388
404
|
*
|
|
389
405
|
* Syntax highlighting is layered on as pure paint, never layout: the `<code>`
|
|
390
406
|
* text renders immediately (plain, selectable, accessible), the language is
|
|
@@ -393,6 +409,27 @@ declare namespace Markdown {
|
|
|
393
409
|
* turns `color: transparent` in the same frame. Highlighting therefore can't
|
|
394
410
|
* block first paint and can't ever change the measured height.
|
|
395
411
|
*/
|
|
412
|
+
/**
|
|
413
|
+
* Optional chrome bar above the code: the language on the left, a
|
|
414
|
+
* copy-to-clipboard button on the right. Its fixed `height` is added to the
|
|
415
|
+
* block's measured height, so the block still measures exactly what it paints.
|
|
416
|
+
* Pure decoration otherwise — the bar never wraps and never grows.
|
|
417
|
+
*/
|
|
418
|
+
interface CodeBlockHeader {
|
|
419
|
+
/** Left-aligned label; falls back to {@link CodeBlockProps.lang}, then `code`. */
|
|
420
|
+
label?: string;
|
|
421
|
+
/** Fixed bar height in px (folded into the measured height). */
|
|
422
|
+
height: number;
|
|
423
|
+
/** Label + button font size in px. */
|
|
424
|
+
fontSize: number;
|
|
425
|
+
/** Monospace family for the label/button; defaults to `monospace`. */
|
|
426
|
+
fontFamily?: string;
|
|
427
|
+
background?: string;
|
|
428
|
+
color?: string;
|
|
429
|
+
borderColor?: string;
|
|
430
|
+
/** Copy-button fill. */
|
|
431
|
+
buttonBackground?: string;
|
|
432
|
+
}
|
|
396
433
|
interface CodeBlockProps<C extends string = string> {
|
|
397
434
|
/** Raw code text. Newlines determine the line count. */
|
|
398
435
|
value: string;
|
|
@@ -413,6 +450,11 @@ interface CodeBlockProps<C extends string = string> {
|
|
|
413
450
|
* registered profile are highlighted either way.
|
|
414
451
|
*/
|
|
415
452
|
highlight?: Partial<CodeTokenColors> | false;
|
|
453
|
+
/**
|
|
454
|
+
* A chrome bar above the code (language label + copy button). Omit for the
|
|
455
|
+
* bare `<pre>`. Its `height` is folded into the measured height.
|
|
456
|
+
*/
|
|
457
|
+
header?: CodeBlockHeader;
|
|
416
458
|
style?: MeasurableStyle;
|
|
417
459
|
className?: SafeClassName<C>;
|
|
418
460
|
}
|
|
@@ -495,4 +537,4 @@ declare function registerLanguage(names: string | readonly string[], p: Language
|
|
|
495
537
|
/** The profile for a fence language, or `null` when the language is unknown. */
|
|
496
538
|
declare function profileFor(lang: string | undefined): LanguageProfile | null;
|
|
497
539
|
//#endregion
|
|
498
|
-
export { type Blockquote, type BoxProps, type Code, CodeBlock, type CodeBlockProps, type CodeTokenColors, type DeepPartial, type Font, HStack, type HStackProps, type Heading, type Html, type Image, type InlineFormat, type InlineTextOptions, type LanguageProfile, type Link, type List, type ListItem, Markdown, type MarkdownComponent, type MarkdownComponentProps, type MarkdownComponents, type MarkdownParseOptions, type MarkdownProps, type MarkdownRenderContext, type MarkdownTheme, type Paragraph, type PhrasingContent, type PrimitiveComponent, type RenderMarkdownOptions, type ResolvedMarkdownComponents, RichText, type RichTextProps, type RichTextRun, type Root, type RootContent, type Table, TableBlock, type TableBlockProps, type TableCell, type TableRow, Text, type TextProps, type ThematicBreak, type TokenType, VStack, type VStackProps, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, parseMarkdown, profileFor, registerLanguage, renderMarkdown, resolveTheme };
|
|
540
|
+
export { type Blockquote, type BoxProps, type Code, CodeBlock, type CodeBlockHeader, type CodeBlockProps, type CodeTokenColors, type DeepPartial, type Font, HStack, type HStackProps, type Heading, type Html, type Image, type InlineFormat, type InlineTextOptions, type LanguageProfile, type Link, type List, type ListItem, Markdown, type MarkdownComponent, type MarkdownComponentProps, type MarkdownComponents, type MarkdownParseOptions, type MarkdownProps, type MarkdownRenderContext, type MarkdownTheme, type Paragraph, type PhrasingContent, type PrimitiveComponent, type RenderMarkdownOptions, type ResolvedMarkdownComponents, RichText, type RichTextProps, type RichTextRun, type Root, type RootContent, type Table, TableBlock, type TableBlockProps, type TableCell, type TableRow, Text, type TextProps, type ThematicBreak, type TokenType, VStack, type VStackProps, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, parseMarkdown, profileFor, registerLanguage, renderMarkdown, resolveTheme };
|
package/dist/index.d.mts
CHANGED
|
@@ -110,6 +110,21 @@ interface MarkdownTheme {
|
|
|
110
110
|
* change a block's measured height.
|
|
111
111
|
*/
|
|
112
112
|
highlight: CodeTokenColors | false;
|
|
113
|
+
/**
|
|
114
|
+
* Optional chrome bar above the code — the language on the left, a
|
|
115
|
+
* copy-to-clipboard button on the right. `show: false` (the default) keeps
|
|
116
|
+
* the bare `<pre>`. When shown, the bar's fixed `height` is folded into the
|
|
117
|
+
* block's measured height, so computed and painted heights stay identical.
|
|
118
|
+
*/
|
|
119
|
+
header: {
|
|
120
|
+
/** Render the chrome bar. Off by default. */show: boolean; /** Fixed bar height in px (counted in the measured height). */
|
|
121
|
+
height: number; /** Label + button font size in px. */
|
|
122
|
+
fontSize: number; /** Bar background. */
|
|
123
|
+
background: string; /** Label + button text colour. */
|
|
124
|
+
color: string; /** Bottom hairline + button border colour. */
|
|
125
|
+
borderColor: string; /** Copy-button fill. */
|
|
126
|
+
buttonBackground: string;
|
|
127
|
+
};
|
|
113
128
|
};
|
|
114
129
|
blockquote: {
|
|
115
130
|
padding: number;
|
|
@@ -383,8 +398,9 @@ declare namespace Markdown {
|
|
|
383
398
|
//#region src/primitives/code-block.d.ts
|
|
384
399
|
/**
|
|
385
400
|
* A fenced code block. Code does not wrap — long lines scroll horizontally — so
|
|
386
|
-
* its height is simply `lineCount × lineHeight + 2 × padding
|
|
387
|
-
*
|
|
401
|
+
* its height is simply `lineCount × lineHeight + 2 × padding` (plus an optional
|
|
402
|
+
* fixed-height {@link CodeBlockHeader} bar), independent of the row width. That
|
|
403
|
+
* makes it trivially and exactly measurable.
|
|
388
404
|
*
|
|
389
405
|
* Syntax highlighting is layered on as pure paint, never layout: the `<code>`
|
|
390
406
|
* text renders immediately (plain, selectable, accessible), the language is
|
|
@@ -393,6 +409,27 @@ declare namespace Markdown {
|
|
|
393
409
|
* turns `color: transparent` in the same frame. Highlighting therefore can't
|
|
394
410
|
* block first paint and can't ever change the measured height.
|
|
395
411
|
*/
|
|
412
|
+
/**
|
|
413
|
+
* Optional chrome bar above the code: the language on the left, a
|
|
414
|
+
* copy-to-clipboard button on the right. Its fixed `height` is added to the
|
|
415
|
+
* block's measured height, so the block still measures exactly what it paints.
|
|
416
|
+
* Pure decoration otherwise — the bar never wraps and never grows.
|
|
417
|
+
*/
|
|
418
|
+
interface CodeBlockHeader {
|
|
419
|
+
/** Left-aligned label; falls back to {@link CodeBlockProps.lang}, then `code`. */
|
|
420
|
+
label?: string;
|
|
421
|
+
/** Fixed bar height in px (folded into the measured height). */
|
|
422
|
+
height: number;
|
|
423
|
+
/** Label + button font size in px. */
|
|
424
|
+
fontSize: number;
|
|
425
|
+
/** Monospace family for the label/button; defaults to `monospace`. */
|
|
426
|
+
fontFamily?: string;
|
|
427
|
+
background?: string;
|
|
428
|
+
color?: string;
|
|
429
|
+
borderColor?: string;
|
|
430
|
+
/** Copy-button fill. */
|
|
431
|
+
buttonBackground?: string;
|
|
432
|
+
}
|
|
396
433
|
interface CodeBlockProps<C extends string = string> {
|
|
397
434
|
/** Raw code text. Newlines determine the line count. */
|
|
398
435
|
value: string;
|
|
@@ -413,6 +450,11 @@ interface CodeBlockProps<C extends string = string> {
|
|
|
413
450
|
* registered profile are highlighted either way.
|
|
414
451
|
*/
|
|
415
452
|
highlight?: Partial<CodeTokenColors> | false;
|
|
453
|
+
/**
|
|
454
|
+
* A chrome bar above the code (language label + copy button). Omit for the
|
|
455
|
+
* bare `<pre>`. Its `height` is folded into the measured height.
|
|
456
|
+
*/
|
|
457
|
+
header?: CodeBlockHeader;
|
|
416
458
|
style?: MeasurableStyle;
|
|
417
459
|
className?: SafeClassName<C>;
|
|
418
460
|
}
|
|
@@ -495,4 +537,4 @@ declare function registerLanguage(names: string | readonly string[], p: Language
|
|
|
495
537
|
/** The profile for a fence language, or `null` when the language is unknown. */
|
|
496
538
|
declare function profileFor(lang: string | undefined): LanguageProfile | null;
|
|
497
539
|
//#endregion
|
|
498
|
-
export { type Blockquote, type BoxProps, type Code, CodeBlock, type CodeBlockProps, type CodeTokenColors, type DeepPartial, type Font, HStack, type HStackProps, type Heading, type Html, type Image, type InlineFormat, type InlineTextOptions, type LanguageProfile, type Link, type List, type ListItem, Markdown, type MarkdownComponent, type MarkdownComponentProps, type MarkdownComponents, type MarkdownParseOptions, type MarkdownProps, type MarkdownRenderContext, type MarkdownTheme, type Paragraph, type PhrasingContent, type PrimitiveComponent, type RenderMarkdownOptions, type ResolvedMarkdownComponents, RichText, type RichTextProps, type RichTextRun, type Root, type RootContent, type Table, TableBlock, type TableBlockProps, type TableCell, type TableRow, Text, type TextProps, type ThematicBreak, type TokenType, VStack, type VStackProps, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, parseMarkdown, profileFor, registerLanguage, renderMarkdown, resolveTheme };
|
|
540
|
+
export { type Blockquote, type BoxProps, type Code, CodeBlock, type CodeBlockHeader, type CodeBlockProps, type CodeTokenColors, type DeepPartial, type Font, HStack, type HStackProps, type Heading, type Html, type Image, type InlineFormat, type InlineTextOptions, type LanguageProfile, type Link, type List, type ListItem, Markdown, type MarkdownComponent, type MarkdownComponentProps, type MarkdownComponents, type MarkdownParseOptions, type MarkdownProps, type MarkdownRenderContext, type MarkdownTheme, type Paragraph, type PhrasingContent, type PrimitiveComponent, type RenderMarkdownOptions, type ResolvedMarkdownComponents, RichText, type RichTextProps, type RichTextRun, type Root, type RootContent, type Table, TableBlock, type TableBlockProps, type TableCell, type TableRow, Text, type TextProps, type ThematicBreak, type TokenType, VStack, type VStackProps, baseFormat, clearParseCache, clearRichTextCache, composeFont, defaultComponents, defaultTheme, defaultTokenColors, defineMarkdownComponents, definePrimitive, flattenInline, parseMarkdown, profileFor, registerLanguage, renderMarkdown, resolveTheme };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment, createElement, useEffect, useLayoutEffect, useRef } from "react";
|
|
1
|
+
import { Fragment, createElement, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { HStack, HStack as HStack$1, Text, VStack, VStack as VStack$1, assertMeasurableFont, definePrimitive, definePrimitive as definePrimitive$1, fontEpoch, fontLonghands, fontWithLineHeight, markPrimitive, naturalWidthOf } from "@wingleeio/mugen";
|
|
3
3
|
import { createIncremarkParser } from "@incremark/core";
|
|
4
4
|
import { measureRichInlineStats, prepareRichInline } from "@chenglou/pretext/rich-inline";
|
|
@@ -161,7 +161,16 @@ const defaultTheme = {
|
|
|
161
161
|
background: "rgba(127, 127, 127, 0.12)",
|
|
162
162
|
color: "inherit",
|
|
163
163
|
radius: 8,
|
|
164
|
-
highlight: defaultTokenColors
|
|
164
|
+
highlight: defaultTokenColors,
|
|
165
|
+
header: {
|
|
166
|
+
show: false,
|
|
167
|
+
height: 38,
|
|
168
|
+
fontSize: 12,
|
|
169
|
+
background: "rgba(127, 127, 127, 0.06)",
|
|
170
|
+
color: "rgba(127, 127, 127, 0.85)",
|
|
171
|
+
borderColor: "rgba(127, 127, 127, 0.18)",
|
|
172
|
+
buttonBackground: "rgba(127, 127, 127, 0.04)"
|
|
173
|
+
}
|
|
165
174
|
},
|
|
166
175
|
blockquote: {
|
|
167
176
|
padding: 14,
|
|
@@ -1310,7 +1319,7 @@ function lineCount(value) {
|
|
|
1310
1319
|
function measureCodeBlock(props, ctx) {
|
|
1311
1320
|
assertMeasurableFont(props.font);
|
|
1312
1321
|
const pad = props.padding ?? 0;
|
|
1313
|
-
return lineCount(props.value) * props.lineHeight + 2 * pad;
|
|
1322
|
+
return (props.header ? props.header.height : 0) + lineCount(props.value) * props.lineHeight + 2 * pad;
|
|
1314
1323
|
}
|
|
1315
1324
|
const colorsCache = /* @__PURE__ */ new WeakMap();
|
|
1316
1325
|
function resolveTokenColors(overrides) {
|
|
@@ -1374,9 +1383,124 @@ function HighlightedCode(props) {
|
|
|
1374
1383
|
style: overlayStyle
|
|
1375
1384
|
}));
|
|
1376
1385
|
}
|
|
1386
|
+
function legacyCopy(text) {
|
|
1387
|
+
if (typeof document === "undefined") return false;
|
|
1388
|
+
const ta = document.createElement("textarea");
|
|
1389
|
+
ta.value = text;
|
|
1390
|
+
ta.setAttribute("readonly", "");
|
|
1391
|
+
ta.style.position = "fixed";
|
|
1392
|
+
ta.style.top = "0";
|
|
1393
|
+
ta.style.left = "0";
|
|
1394
|
+
ta.style.opacity = "0";
|
|
1395
|
+
ta.style.pointerEvents = "none";
|
|
1396
|
+
document.body.appendChild(ta);
|
|
1397
|
+
ta.select();
|
|
1398
|
+
let ok = false;
|
|
1399
|
+
try {
|
|
1400
|
+
ok = document.execCommand("copy");
|
|
1401
|
+
} catch {
|
|
1402
|
+
ok = false;
|
|
1403
|
+
}
|
|
1404
|
+
document.body.removeChild(ta);
|
|
1405
|
+
return ok;
|
|
1406
|
+
}
|
|
1407
|
+
async function copyText(text) {
|
|
1408
|
+
const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
1409
|
+
if (clip?.writeText != null) try {
|
|
1410
|
+
await clip.writeText(text);
|
|
1411
|
+
return true;
|
|
1412
|
+
} catch {}
|
|
1413
|
+
return legacyCopy(text);
|
|
1414
|
+
}
|
|
1415
|
+
/** Copies the block's raw text; flips to "Copied" for ~1.6s on success. */
|
|
1416
|
+
function CopyButton(props) {
|
|
1417
|
+
const [copied, setCopied] = useState(false);
|
|
1418
|
+
const [hovered, setHovered] = useState(false);
|
|
1419
|
+
const timer = useRef(null);
|
|
1420
|
+
useEffect(() => () => {
|
|
1421
|
+
if (timer.current != null) clearTimeout(timer.current);
|
|
1422
|
+
}, []);
|
|
1423
|
+
const onClick = () => {
|
|
1424
|
+
copyText(props.value).then((ok) => {
|
|
1425
|
+
if (!ok) return;
|
|
1426
|
+
setCopied(true);
|
|
1427
|
+
if (timer.current != null) clearTimeout(timer.current);
|
|
1428
|
+
timer.current = setTimeout(() => setCopied(false), 1600);
|
|
1429
|
+
}).catch(() => {});
|
|
1430
|
+
};
|
|
1431
|
+
const style = {
|
|
1432
|
+
display: "inline-flex",
|
|
1433
|
+
flex: "0 0 auto",
|
|
1434
|
+
alignItems: "center",
|
|
1435
|
+
justifyContent: "center",
|
|
1436
|
+
whiteSpace: "nowrap",
|
|
1437
|
+
cursor: "pointer",
|
|
1438
|
+
borderRadius: 8,
|
|
1439
|
+
border: `1px solid ${props.borderColor ?? "rgba(127, 127, 127, 0.2)"}`,
|
|
1440
|
+
...props.background != null ? { background: props.background } : null,
|
|
1441
|
+
...props.color != null ? { color: props.color } : null,
|
|
1442
|
+
padding: "4px 9px",
|
|
1443
|
+
minWidth: "4.6em",
|
|
1444
|
+
fontFamily: "inherit",
|
|
1445
|
+
fontSize: `${props.fontSize}px`,
|
|
1446
|
+
lineHeight: 1,
|
|
1447
|
+
opacity: hovered || copied ? 1 : .82,
|
|
1448
|
+
transition: "opacity 120ms ease"
|
|
1449
|
+
};
|
|
1450
|
+
return createElement("button", {
|
|
1451
|
+
type: "button",
|
|
1452
|
+
onClick,
|
|
1453
|
+
onMouseEnter: () => setHovered(true),
|
|
1454
|
+
onMouseLeave: () => setHovered(false),
|
|
1455
|
+
"aria-label": copied ? "Copied" : "Copy code",
|
|
1456
|
+
style
|
|
1457
|
+
}, copied ? "Copied" : "Copy");
|
|
1458
|
+
}
|
|
1459
|
+
/** The fixed-height chrome bar: language label left, copy button right. */
|
|
1460
|
+
function CodeHeader(props) {
|
|
1461
|
+
const barStyle = {
|
|
1462
|
+
display: "flex",
|
|
1463
|
+
flex: "0 0 auto",
|
|
1464
|
+
alignItems: "center",
|
|
1465
|
+
justifyContent: "space-between",
|
|
1466
|
+
gap: 8,
|
|
1467
|
+
height: `${props.height}px`,
|
|
1468
|
+
boxSizing: "border-box",
|
|
1469
|
+
padding: "0 12px",
|
|
1470
|
+
fontFamily: props.fontFamily,
|
|
1471
|
+
...props.radius != null ? {
|
|
1472
|
+
borderTopLeftRadius: `${props.radius}px`,
|
|
1473
|
+
borderTopRightRadius: `${props.radius}px`
|
|
1474
|
+
} : null,
|
|
1475
|
+
...props.background != null ? { background: props.background } : null,
|
|
1476
|
+
...props.borderColor != null ? { borderBottom: `1px solid ${props.borderColor}` } : null
|
|
1477
|
+
};
|
|
1478
|
+
const labelStyle = {
|
|
1479
|
+
minWidth: 0,
|
|
1480
|
+
overflow: "hidden",
|
|
1481
|
+
textOverflow: "ellipsis",
|
|
1482
|
+
whiteSpace: "nowrap",
|
|
1483
|
+
fontSize: `${props.fontSize}px`,
|
|
1484
|
+
letterSpacing: "0.02em",
|
|
1485
|
+
fontVariantLigatures: "none",
|
|
1486
|
+
...props.color != null ? { color: props.color } : null
|
|
1487
|
+
};
|
|
1488
|
+
return createElement("div", { style: barStyle }, createElement("span", {
|
|
1489
|
+
key: "lang",
|
|
1490
|
+
style: labelStyle
|
|
1491
|
+
}, props.label), createElement(CopyButton, {
|
|
1492
|
+
key: "copy",
|
|
1493
|
+
value: props.value,
|
|
1494
|
+
fontSize: props.fontSize,
|
|
1495
|
+
color: props.color,
|
|
1496
|
+
borderColor: props.borderColor,
|
|
1497
|
+
background: props.buttonBackground
|
|
1498
|
+
}));
|
|
1499
|
+
}
|
|
1377
1500
|
function renderCodeBlock(props) {
|
|
1378
1501
|
const pad = props.padding ?? 0;
|
|
1379
1502
|
const profile = props.highlight === false ? null : profileFor(props.lang);
|
|
1503
|
+
const header = props.header;
|
|
1380
1504
|
const preStyle = {
|
|
1381
1505
|
margin: 0,
|
|
1382
1506
|
padding: `${pad}px`,
|
|
@@ -1385,7 +1509,10 @@ function renderCodeBlock(props) {
|
|
|
1385
1509
|
boxSizing: "border-box",
|
|
1386
1510
|
...props.background != null ? { background: props.background } : null,
|
|
1387
1511
|
...props.color != null ? { color: props.color } : null,
|
|
1388
|
-
...props.radius != null ? { borderRadius: `${props.radius}px` } :
|
|
1512
|
+
...props.radius != null ? header == null ? { borderRadius: `${props.radius}px` } : {
|
|
1513
|
+
borderBottomLeftRadius: `${props.radius}px`,
|
|
1514
|
+
borderBottomRightRadius: `${props.radius}px`
|
|
1515
|
+
} : null,
|
|
1389
1516
|
...profile != null ? {
|
|
1390
1517
|
position: "relative",
|
|
1391
1518
|
tabSize: 8
|
|
@@ -1398,17 +1525,13 @@ function renderCodeBlock(props) {
|
|
|
1398
1525
|
margin: 0,
|
|
1399
1526
|
padding: 0
|
|
1400
1527
|
};
|
|
1401
|
-
|
|
1402
|
-
className: props.className,
|
|
1528
|
+
const pre = createElement("pre", {
|
|
1529
|
+
...header == null ? { className: props.className } : null,
|
|
1403
1530
|
style: preStyle
|
|
1404
|
-
}, createElement("code", {
|
|
1531
|
+
}, profile == null ? createElement("code", {
|
|
1405
1532
|
style: codeStyle,
|
|
1406
1533
|
...props.lang ? { "data-lang": props.lang } : null
|
|
1407
|
-
}, props.value)
|
|
1408
|
-
return createElement("pre", {
|
|
1409
|
-
className: props.className,
|
|
1410
|
-
style: preStyle
|
|
1411
|
-
}, createElement(HighlightedCode, {
|
|
1534
|
+
}, props.value) : createElement(HighlightedCode, {
|
|
1412
1535
|
value: props.value,
|
|
1413
1536
|
lang: props.lang,
|
|
1414
1537
|
font: props.font,
|
|
@@ -1418,6 +1541,20 @@ function renderCodeBlock(props) {
|
|
|
1418
1541
|
colors: resolveTokenColors(props.highlight === false ? void 0 : props.highlight),
|
|
1419
1542
|
codeStyle
|
|
1420
1543
|
}));
|
|
1544
|
+
if (header == null) return pre;
|
|
1545
|
+
return createElement("div", { className: props.className }, createElement(CodeHeader, {
|
|
1546
|
+
key: "header",
|
|
1547
|
+
label: header.label ?? props.lang ?? "code",
|
|
1548
|
+
value: props.value,
|
|
1549
|
+
height: header.height,
|
|
1550
|
+
fontSize: header.fontSize,
|
|
1551
|
+
fontFamily: header.fontFamily ?? "monospace",
|
|
1552
|
+
...props.radius != null ? { radius: props.radius } : null,
|
|
1553
|
+
background: header.background,
|
|
1554
|
+
color: header.color,
|
|
1555
|
+
borderColor: header.borderColor,
|
|
1556
|
+
buttonBackground: header.buttonBackground
|
|
1557
|
+
}), pre);
|
|
1421
1558
|
}
|
|
1422
1559
|
/** A measurable fenced-code primitive (no wrapping; height from line count). */
|
|
1423
1560
|
const CodeBlock = markPrimitive(renderCodeBlock, {
|
|
@@ -1631,7 +1768,16 @@ const defaultComponents = {
|
|
|
1631
1768
|
background: c.background,
|
|
1632
1769
|
radius: c.radius,
|
|
1633
1770
|
highlight: c.highlight,
|
|
1634
|
-
...c.color !== "inherit" ? { color: c.color } : null
|
|
1771
|
+
...c.color !== "inherit" ? { color: c.color } : null,
|
|
1772
|
+
...c.header.show ? { header: {
|
|
1773
|
+
height: c.header.height,
|
|
1774
|
+
fontSize: c.header.fontSize,
|
|
1775
|
+
fontFamily: ctx.theme.monoFamily,
|
|
1776
|
+
background: c.header.background,
|
|
1777
|
+
color: c.header.color,
|
|
1778
|
+
borderColor: c.header.borderColor,
|
|
1779
|
+
buttonBackground: c.header.buttonBackground
|
|
1780
|
+
} } : null
|
|
1635
1781
|
});
|
|
1636
1782
|
},
|
|
1637
1783
|
list: ({ node, ctx }) => renderList(node, ctx),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wingleeio/mugen-markdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Measurable markdown for mugen — incremark-parsed, rendered with mugen primitives so the virtualizer's tree walker computes exact row heights.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Wing Lee <contact@winglee.io>",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"tsdown": "^0.22.2",
|
|
66
66
|
"typescript": "^6.0.3",
|
|
67
67
|
"vitest": "^4.1.8",
|
|
68
|
-
"@wingleeio/mugen": "0.3.
|
|
68
|
+
"@wingleeio/mugen": "0.3.4"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsdown",
|