@vectojs/markdown 0.18.1 → 0.19.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/Markdown.d.ts +96 -8
- package/dist/blockAffordances.d.ts +60 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +635 -71
- package/dist/index.mjs +633 -71
- package/dist/markdown-code.d.ts +136 -5
- package/dist/theme.d.ts +21 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ __export(index_exports, {
|
|
|
44
44
|
extensionForLanguage: () => extensionForLanguage,
|
|
45
45
|
footnoteMarker: () => footnoteMarker,
|
|
46
46
|
hasFencedBlockRenderer: () => hasFencedBlockRenderer,
|
|
47
|
+
highlightedLanguages: () => highlightedLanguages,
|
|
47
48
|
isFencedBlockRendererReady: () => isFencedBlockRendererReady,
|
|
48
49
|
isMathJaxReady: () => isMathJaxReady,
|
|
49
50
|
isPresetName: () => isPresetName,
|
|
@@ -52,6 +53,7 @@ __export(index_exports, {
|
|
|
52
53
|
preloadMathJax: () => preloadMathJax,
|
|
53
54
|
registerFencedBlockRenderer: () => registerFencedBlockRenderer,
|
|
54
55
|
renderFencedBlock: () => renderFencedBlock,
|
|
56
|
+
resolveBlockAffordanceConfig: () => resolveBlockAffordanceConfig,
|
|
55
57
|
resolvePresetTheme: () => resolvePresetTheme,
|
|
56
58
|
scanFrontMatter: () => scanFrontMatter,
|
|
57
59
|
tableContentOf: () => tableContentOf,
|
|
@@ -606,11 +608,13 @@ var DEFAULT_THEME = {
|
|
|
606
608
|
syntaxStringColor: "#86efac",
|
|
607
609
|
syntaxCommentColor: "#64748b",
|
|
608
610
|
syntaxNumberColor: "#fbbf24",
|
|
611
|
+
codeLangColor: "#64748b",
|
|
609
612
|
bodyFont: "Inter, system-ui, sans-serif",
|
|
610
613
|
codeFont: 'ui-monospace, "JetBrains Mono", "Fira Code", monospace',
|
|
611
614
|
fontSize: 16,
|
|
612
615
|
headingSizes: [32, 28, 24, 20, 18, 16],
|
|
613
616
|
codeFontSize: 15,
|
|
617
|
+
codeLangFontSize: 12,
|
|
614
618
|
tableFontSize: 14,
|
|
615
619
|
footnoteMarkerScale: 0.75,
|
|
616
620
|
subscriptScale: 0.75,
|
|
@@ -645,6 +649,12 @@ function resolveTheme(theme) {
|
|
|
645
649
|
if (theme?.footnoteColor === void 0) {
|
|
646
650
|
merged.footnoteColor = merged.linkColor;
|
|
647
651
|
}
|
|
652
|
+
if (theme?.codeLangColor === void 0) {
|
|
653
|
+
merged.codeLangColor = merged.syntaxCommentColor;
|
|
654
|
+
}
|
|
655
|
+
if (theme?.codeLangFontSize === void 0) {
|
|
656
|
+
merged.codeLangFontSize = Math.max(1, merged.codeFontSize - 3);
|
|
657
|
+
}
|
|
648
658
|
return merged;
|
|
649
659
|
}
|
|
650
660
|
function headingSize(theme, depth) {
|
|
@@ -984,14 +994,215 @@ var KEYWORD_SETS = {
|
|
|
984
994
|
"static"
|
|
985
995
|
])
|
|
986
996
|
};
|
|
997
|
+
KEYWORD_SETS["bash"] = /* @__PURE__ */ new Set([
|
|
998
|
+
// Shell builtins and control words. Deliberately not the whole of coreutils:
|
|
999
|
+
// a keyword table that includes every command name colors an entire script
|
|
1000
|
+
// uniformly, which reads worse than coloring only the control flow.
|
|
1001
|
+
"if",
|
|
1002
|
+
"then",
|
|
1003
|
+
"else",
|
|
1004
|
+
"elif",
|
|
1005
|
+
"fi",
|
|
1006
|
+
"for",
|
|
1007
|
+
"while",
|
|
1008
|
+
"until",
|
|
1009
|
+
"do",
|
|
1010
|
+
"done",
|
|
1011
|
+
"case",
|
|
1012
|
+
"esac",
|
|
1013
|
+
"in",
|
|
1014
|
+
"function",
|
|
1015
|
+
"return",
|
|
1016
|
+
"exit",
|
|
1017
|
+
"break",
|
|
1018
|
+
"continue",
|
|
1019
|
+
"local",
|
|
1020
|
+
"export",
|
|
1021
|
+
"readonly",
|
|
1022
|
+
"declare",
|
|
1023
|
+
"unset",
|
|
1024
|
+
"shift",
|
|
1025
|
+
"source",
|
|
1026
|
+
"alias",
|
|
1027
|
+
"set",
|
|
1028
|
+
"trap",
|
|
1029
|
+
"echo",
|
|
1030
|
+
"cd",
|
|
1031
|
+
"sudo",
|
|
1032
|
+
"true",
|
|
1033
|
+
"false"
|
|
1034
|
+
]);
|
|
1035
|
+
KEYWORD_SETS["json"] = /* @__PURE__ */ new Set(["true", "false", "null"]);
|
|
1036
|
+
KEYWORD_SETS["css"] = /* @__PURE__ */ new Set([
|
|
1037
|
+
"important",
|
|
1038
|
+
"inherit",
|
|
1039
|
+
"initial",
|
|
1040
|
+
"unset",
|
|
1041
|
+
"revert",
|
|
1042
|
+
"auto",
|
|
1043
|
+
"none",
|
|
1044
|
+
"var",
|
|
1045
|
+
"calc"
|
|
1046
|
+
]);
|
|
1047
|
+
KEYWORD_SETS["html"] = /* @__PURE__ */ new Set([
|
|
1048
|
+
// Tag names are the meaningful tokens a reader scans for. The tokenizer is
|
|
1049
|
+
// word-based, so `<div>` yields the word `div`.
|
|
1050
|
+
"html",
|
|
1051
|
+
"head",
|
|
1052
|
+
"body",
|
|
1053
|
+
"title",
|
|
1054
|
+
"meta",
|
|
1055
|
+
"link",
|
|
1056
|
+
"script",
|
|
1057
|
+
"style",
|
|
1058
|
+
"div",
|
|
1059
|
+
"span",
|
|
1060
|
+
"p",
|
|
1061
|
+
"a",
|
|
1062
|
+
"img",
|
|
1063
|
+
"ul",
|
|
1064
|
+
"ol",
|
|
1065
|
+
"li",
|
|
1066
|
+
"table",
|
|
1067
|
+
"tr",
|
|
1068
|
+
"td",
|
|
1069
|
+
"th",
|
|
1070
|
+
"form",
|
|
1071
|
+
"input",
|
|
1072
|
+
"button",
|
|
1073
|
+
"label",
|
|
1074
|
+
"select",
|
|
1075
|
+
"option",
|
|
1076
|
+
"textarea",
|
|
1077
|
+
"header",
|
|
1078
|
+
"footer",
|
|
1079
|
+
"nav",
|
|
1080
|
+
"main",
|
|
1081
|
+
"section",
|
|
1082
|
+
"article",
|
|
1083
|
+
"aside",
|
|
1084
|
+
"canvas",
|
|
1085
|
+
"svg",
|
|
1086
|
+
"template",
|
|
1087
|
+
"slot"
|
|
1088
|
+
]);
|
|
987
1089
|
KEYWORD_SETS["javascript"] = KEYWORD_SETS["js"];
|
|
988
1090
|
KEYWORD_SETS["typescript"] = KEYWORD_SETS["ts"];
|
|
989
1091
|
KEYWORD_SETS["python"] = KEYWORD_SETS["py"];
|
|
990
1092
|
KEYWORD_SETS["rs"] = KEYWORD_SETS["rust"];
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1093
|
+
KEYWORD_SETS["jsx"] = KEYWORD_SETS["js"];
|
|
1094
|
+
KEYWORD_SETS["mjs"] = KEYWORD_SETS["js"];
|
|
1095
|
+
KEYWORD_SETS["cjs"] = KEYWORD_SETS["js"];
|
|
1096
|
+
KEYWORD_SETS["tsx"] = KEYWORD_SETS["ts"];
|
|
1097
|
+
KEYWORD_SETS["mts"] = KEYWORD_SETS["ts"];
|
|
1098
|
+
KEYWORD_SETS["cts"] = KEYWORD_SETS["ts"];
|
|
1099
|
+
KEYWORD_SETS["sh"] = KEYWORD_SETS["bash"];
|
|
1100
|
+
KEYWORD_SETS["zsh"] = KEYWORD_SETS["bash"];
|
|
1101
|
+
KEYWORD_SETS["shell"] = KEYWORD_SETS["bash"];
|
|
1102
|
+
KEYWORD_SETS["console"] = KEYWORD_SETS["bash"];
|
|
1103
|
+
KEYWORD_SETS["jsonc"] = KEYWORD_SETS["json"];
|
|
1104
|
+
KEYWORD_SETS["json5"] = KEYWORD_SETS["json"];
|
|
1105
|
+
KEYWORD_SETS["scss"] = KEYWORD_SETS["css"];
|
|
1106
|
+
KEYWORD_SETS["sass"] = KEYWORD_SETS["css"];
|
|
1107
|
+
KEYWORD_SETS["less"] = KEYWORD_SETS["css"];
|
|
1108
|
+
KEYWORD_SETS["vue"] = KEYWORD_SETS["html"];
|
|
1109
|
+
KEYWORD_SETS["svelte"] = KEYWORD_SETS["html"];
|
|
1110
|
+
KEYWORD_SETS["xml"] = KEYWORD_SETS["html"];
|
|
1111
|
+
KEYWORD_SETS["svg"] = KEYWORD_SETS["html"];
|
|
1112
|
+
var C_LIKE = {
|
|
1113
|
+
lineComments: ["//"],
|
|
1114
|
+
quotes: ['"', "'", "`"],
|
|
1115
|
+
numbers: true,
|
|
1116
|
+
blockComments: [["/*", "*/"]],
|
|
1117
|
+
// A JS/TS template literal spans lines. Listed here as well as in `quotes`:
|
|
1118
|
+
// `quotes` handles the common single-line case, and this carries the rest.
|
|
1119
|
+
multilineStrings: ["`"]
|
|
1120
|
+
};
|
|
1121
|
+
var HASH_COMMENT = {
|
|
1122
|
+
lineComments: ["#"],
|
|
1123
|
+
quotes: ['"', "'"],
|
|
1124
|
+
numbers: true
|
|
1125
|
+
};
|
|
1126
|
+
var LANGUAGE_SYNTAX = {
|
|
1127
|
+
js: C_LIKE,
|
|
1128
|
+
ts: C_LIKE,
|
|
1129
|
+
// A Python docstring is the language's block comment in practice, and it is
|
|
1130
|
+
// lexically a string, so it is carried as one rather than invented as a third
|
|
1131
|
+
// kind. Triple delimiters are listed before the single ones so the longest
|
|
1132
|
+
// match wins.
|
|
1133
|
+
py: { ...HASH_COMMENT, multilineStrings: ['"""', "'''"] },
|
|
1134
|
+
// Rust has `//` line comments AND `'` lifetimes. The unterminated-quote
|
|
1135
|
+
// fallback already keeps a lifetime from swallowing the line, so `'` stays
|
|
1136
|
+
// listed: `'a'` is a valid char literal and should color as a string.
|
|
1137
|
+
rust: C_LIKE,
|
|
1138
|
+
bash: HASH_COMMENT,
|
|
1139
|
+
// JSON has no comments and no single-quoted strings. JSONC does have `//`,
|
|
1140
|
+
// and is aliased separately below rather than sharing this entry.
|
|
1141
|
+
json: { lineComments: [], quotes: ['"'], numbers: true },
|
|
1142
|
+
// CSS has only block comments — which now span lines, so this entry claims
|
|
1143
|
+
// them. Numbers are everywhere in CSS and coloring them is most of the visible
|
|
1144
|
+
// benefit.
|
|
1145
|
+
css: {
|
|
1146
|
+
lineComments: [],
|
|
1147
|
+
quotes: ['"', "'"],
|
|
1148
|
+
numbers: true,
|
|
1149
|
+
blockComments: [["/*", "*/"]]
|
|
1150
|
+
},
|
|
1151
|
+
// Markup: no line comments, and numbers inside attribute values are noise
|
|
1152
|
+
// rather than signal. An SGML comment spans lines like any other block form.
|
|
1153
|
+
html: {
|
|
1154
|
+
lineComments: [],
|
|
1155
|
+
quotes: ['"', "'"],
|
|
1156
|
+
numbers: false,
|
|
1157
|
+
blockComments: [["<!--", "-->"]]
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
LANGUAGE_SYNTAX["javascript"] = LANGUAGE_SYNTAX["js"];
|
|
1161
|
+
LANGUAGE_SYNTAX["typescript"] = LANGUAGE_SYNTAX["ts"];
|
|
1162
|
+
LANGUAGE_SYNTAX["python"] = LANGUAGE_SYNTAX["py"];
|
|
1163
|
+
LANGUAGE_SYNTAX["rs"] = LANGUAGE_SYNTAX["rust"];
|
|
1164
|
+
LANGUAGE_SYNTAX["jsx"] = LANGUAGE_SYNTAX["js"];
|
|
1165
|
+
LANGUAGE_SYNTAX["mjs"] = LANGUAGE_SYNTAX["js"];
|
|
1166
|
+
LANGUAGE_SYNTAX["cjs"] = LANGUAGE_SYNTAX["js"];
|
|
1167
|
+
LANGUAGE_SYNTAX["tsx"] = LANGUAGE_SYNTAX["ts"];
|
|
1168
|
+
LANGUAGE_SYNTAX["mts"] = LANGUAGE_SYNTAX["ts"];
|
|
1169
|
+
LANGUAGE_SYNTAX["cts"] = LANGUAGE_SYNTAX["ts"];
|
|
1170
|
+
LANGUAGE_SYNTAX["sh"] = LANGUAGE_SYNTAX["bash"];
|
|
1171
|
+
LANGUAGE_SYNTAX["zsh"] = LANGUAGE_SYNTAX["bash"];
|
|
1172
|
+
LANGUAGE_SYNTAX["shell"] = LANGUAGE_SYNTAX["bash"];
|
|
1173
|
+
LANGUAGE_SYNTAX["console"] = LANGUAGE_SYNTAX["bash"];
|
|
1174
|
+
LANGUAGE_SYNTAX["yaml"] = HASH_COMMENT;
|
|
1175
|
+
LANGUAGE_SYNTAX["yml"] = HASH_COMMENT;
|
|
1176
|
+
LANGUAGE_SYNTAX["toml"] = HASH_COMMENT;
|
|
1177
|
+
LANGUAGE_SYNTAX["ini"] = HASH_COMMENT;
|
|
1178
|
+
LANGUAGE_SYNTAX["dockerfile"] = HASH_COMMENT;
|
|
1179
|
+
LANGUAGE_SYNTAX["makefile"] = HASH_COMMENT;
|
|
1180
|
+
LANGUAGE_SYNTAX["make"] = HASH_COMMENT;
|
|
1181
|
+
LANGUAGE_SYNTAX["jsonc"] = { lineComments: ["//"], quotes: ['"'], numbers: true };
|
|
1182
|
+
LANGUAGE_SYNTAX["json5"] = { lineComments: ["//"], quotes: ['"', "'"], numbers: true };
|
|
1183
|
+
LANGUAGE_SYNTAX["scss"] = C_LIKE;
|
|
1184
|
+
LANGUAGE_SYNTAX["sass"] = C_LIKE;
|
|
1185
|
+
LANGUAGE_SYNTAX["less"] = C_LIKE;
|
|
1186
|
+
LANGUAGE_SYNTAX["glsl"] = C_LIKE;
|
|
1187
|
+
LANGUAGE_SYNTAX["c"] = C_LIKE;
|
|
1188
|
+
LANGUAGE_SYNTAX["cpp"] = C_LIKE;
|
|
1189
|
+
LANGUAGE_SYNTAX["go"] = C_LIKE;
|
|
1190
|
+
LANGUAGE_SYNTAX["java"] = C_LIKE;
|
|
1191
|
+
LANGUAGE_SYNTAX["kotlin"] = C_LIKE;
|
|
1192
|
+
LANGUAGE_SYNTAX["swift"] = C_LIKE;
|
|
1193
|
+
LANGUAGE_SYNTAX["vue"] = LANGUAGE_SYNTAX["html"];
|
|
1194
|
+
LANGUAGE_SYNTAX["svelte"] = LANGUAGE_SYNTAX["html"];
|
|
1195
|
+
LANGUAGE_SYNTAX["xml"] = LANGUAGE_SYNTAX["html"];
|
|
1196
|
+
LANGUAGE_SYNTAX["svg"] = LANGUAGE_SYNTAX["html"];
|
|
1197
|
+
function highlightedLanguages() {
|
|
1198
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(LANGUAGE_SYNTAX), ...Object.keys(KEYWORD_SETS)])].sort();
|
|
1199
|
+
}
|
|
1200
|
+
function highlightLine(line, lang, theme, carry = null) {
|
|
1201
|
+
const key = lang.trim().toLowerCase().split(/[\s:,{]/)[0] ?? "";
|
|
1202
|
+
const keywords = KEYWORD_SETS[key];
|
|
1203
|
+
const syntax = LANGUAGE_SYNTAX[key];
|
|
1204
|
+
if (!keywords && !syntax) {
|
|
1205
|
+
return { segments: [{ text: line, color: theme.codeColor }], carry: null };
|
|
995
1206
|
}
|
|
996
1207
|
const segments = [];
|
|
997
1208
|
const KEYWORD_COLOR = theme.syntaxKeywordColor;
|
|
@@ -1006,19 +1217,63 @@ function highlightLine(line, lang, theme) {
|
|
|
1006
1217
|
buf = "";
|
|
1007
1218
|
}
|
|
1008
1219
|
};
|
|
1220
|
+
const lexical = syntax ?? C_LIKE;
|
|
1221
|
+
const findClose = (from, close, isString) => {
|
|
1222
|
+
let j = from;
|
|
1223
|
+
while (j < line.length) {
|
|
1224
|
+
if (isString && line[j] === "\\") {
|
|
1225
|
+
j += 2;
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (line.startsWith(close, j)) return j + close.length;
|
|
1229
|
+
j++;
|
|
1230
|
+
}
|
|
1231
|
+
return -1;
|
|
1232
|
+
};
|
|
1233
|
+
if (carry) {
|
|
1234
|
+
const color = carry.kind === "comment" ? COMMENT_COLOR : STRING_COLOR;
|
|
1235
|
+
const end = findClose(0, carry.close, carry.kind === "string");
|
|
1236
|
+
if (end === -1) {
|
|
1237
|
+
if (line.length > 0) segments.push({ text: line, color });
|
|
1238
|
+
return { segments, carry };
|
|
1239
|
+
}
|
|
1240
|
+
segments.push({ text: line.slice(0, end), color });
|
|
1241
|
+
i = end;
|
|
1242
|
+
}
|
|
1009
1243
|
while (i < line.length) {
|
|
1010
1244
|
const ch = line[i];
|
|
1011
|
-
|
|
1245
|
+
const block = lexical.blockComments?.find(([open]) => line.startsWith(open, i));
|
|
1246
|
+
if (block) {
|
|
1247
|
+
const [open, close] = block;
|
|
1012
1248
|
flush(theme.codeColor);
|
|
1013
|
-
|
|
1014
|
-
|
|
1249
|
+
const end = findClose(i + open.length, close, false);
|
|
1250
|
+
if (end === -1) {
|
|
1251
|
+
segments.push({ text: line.slice(i), color: COMMENT_COLOR });
|
|
1252
|
+
return { segments, carry: { kind: "comment", close } };
|
|
1253
|
+
}
|
|
1254
|
+
segments.push({ text: line.slice(i, end), color: COMMENT_COLOR });
|
|
1255
|
+
i = end;
|
|
1256
|
+
continue;
|
|
1015
1257
|
}
|
|
1016
|
-
|
|
1258
|
+
const comment = lexical.lineComments.find((prefix) => line.startsWith(prefix, i));
|
|
1259
|
+
if (comment !== void 0) {
|
|
1017
1260
|
flush(theme.codeColor);
|
|
1018
1261
|
segments.push({ text: line.slice(i), color: COMMENT_COLOR });
|
|
1019
|
-
return segments;
|
|
1262
|
+
return { segments, carry: null };
|
|
1020
1263
|
}
|
|
1021
|
-
|
|
1264
|
+
const multi = lexical.multilineStrings?.find((delim) => line.startsWith(delim, i));
|
|
1265
|
+
if (multi !== void 0) {
|
|
1266
|
+
flush(theme.codeColor);
|
|
1267
|
+
const end = findClose(i + multi.length, multi, true);
|
|
1268
|
+
if (end === -1) {
|
|
1269
|
+
segments.push({ text: line.slice(i), color: STRING_COLOR });
|
|
1270
|
+
return { segments, carry: { kind: "string", close: multi } };
|
|
1271
|
+
}
|
|
1272
|
+
segments.push({ text: line.slice(i, end), color: STRING_COLOR });
|
|
1273
|
+
i = end;
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
if (lexical.quotes.includes(ch)) {
|
|
1022
1277
|
const quote = ch;
|
|
1023
1278
|
let j = i + 1;
|
|
1024
1279
|
let closed = false;
|
|
@@ -1043,7 +1298,7 @@ function highlightLine(line, lang, theme) {
|
|
|
1043
1298
|
i++;
|
|
1044
1299
|
continue;
|
|
1045
1300
|
}
|
|
1046
|
-
if (/\d/.test(ch) && (i === 0 || /[\s(,=+\-*/<>[\]{}:;]/.test(line[i - 1]))) {
|
|
1301
|
+
if (lexical.numbers && /\d/.test(ch) && (i === 0 || /[\s(,=+\-*/<>[\]{}:;]/.test(line[i - 1]))) {
|
|
1047
1302
|
flush(theme.codeColor);
|
|
1048
1303
|
let j = i;
|
|
1049
1304
|
while (j < line.length && /[\d._xXa-fA-F]/.test(line[j])) j++;
|
|
@@ -1058,7 +1313,9 @@ function highlightLine(line, lang, theme) {
|
|
|
1058
1313
|
const word = line.slice(i, j);
|
|
1059
1314
|
segments.push({
|
|
1060
1315
|
text: word,
|
|
1061
|
-
|
|
1316
|
+
// A language may have lexical syntax but no keywords (plain YAML, TOML,
|
|
1317
|
+
// a Dockerfile). Those still get comments, strings and numbers.
|
|
1318
|
+
color: keywords?.has(word) ? KEYWORD_COLOR : theme.codeColor
|
|
1062
1319
|
});
|
|
1063
1320
|
i = j;
|
|
1064
1321
|
continue;
|
|
@@ -1067,10 +1324,18 @@ function highlightLine(line, lang, theme) {
|
|
|
1067
1324
|
i++;
|
|
1068
1325
|
}
|
|
1069
1326
|
flush(theme.codeColor);
|
|
1070
|
-
return segments;
|
|
1327
|
+
return { segments, carry: null };
|
|
1071
1328
|
}
|
|
1072
1329
|
var CodeBlock = class extends import_ui.UIComponent {
|
|
1073
1330
|
lines;
|
|
1331
|
+
/**
|
|
1332
|
+
* Lexical state ENTERING each line, index-aligned with {@link lines}.
|
|
1333
|
+
*
|
|
1334
|
+
* Entering rather than leaving, so a streamed append can resume tokenizing at
|
|
1335
|
+
* the prefix-reuse boundary by reading one entry instead of re-scanning the
|
|
1336
|
+
* document for an unclosed block comment.
|
|
1337
|
+
*/
|
|
1338
|
+
lineCarry = [];
|
|
1074
1339
|
grid = null;
|
|
1075
1340
|
/** Raw (unhighlighted) lines of the last build, for prefix reuse in buildLines. */
|
|
1076
1341
|
rawLines = null;
|
|
@@ -1078,6 +1343,19 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1078
1343
|
source;
|
|
1079
1344
|
/** Bumped by {@link buildLines} and {@link setSelectable}; read by `Scene`. */
|
|
1080
1345
|
contentEpoch = 0;
|
|
1346
|
+
/**
|
|
1347
|
+
* Horizontal scroll offset in local px, always in `[0, maxScrollX]`.
|
|
1348
|
+
*
|
|
1349
|
+
* Code does not wrap, so a line wider than the box would otherwise have an
|
|
1350
|
+
* unreachable tail. This offset is subtracted from BOTH the painted cell x and
|
|
1351
|
+
* the projected line x in the same frame — never one without the other, or the
|
|
1352
|
+
* DOM selection carriers detach from the glyphs they are supposed to cover
|
|
1353
|
+
* (the defect class `5cf7119` and `ee1de6f` fixed on the vertical axis).
|
|
1354
|
+
*/
|
|
1355
|
+
scrollXValue = 0;
|
|
1356
|
+
/** Memoized widest prepared line, keyed by the grid identity it came from. */
|
|
1357
|
+
contentWidthGrid = null;
|
|
1358
|
+
contentWidthValue = 0;
|
|
1081
1359
|
lang;
|
|
1082
1360
|
theme;
|
|
1083
1361
|
/**
|
|
@@ -1089,6 +1367,10 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1089
1367
|
pad;
|
|
1090
1368
|
codeFont;
|
|
1091
1369
|
selectable;
|
|
1370
|
+
/** Whether the language header band is drawn. See {@link CodeBlockOptions.showLanguage}. */
|
|
1371
|
+
showLanguage;
|
|
1372
|
+
/** Font of the header label, resolved once from the theme. */
|
|
1373
|
+
langFont;
|
|
1092
1374
|
/**
|
|
1093
1375
|
* @param theme Any subset of {@link MarkdownTheme}, or the name of a built-in
|
|
1094
1376
|
* preset (see {@link MarkdownThemePresetName}). Accepting a partial theme
|
|
@@ -1100,7 +1382,7 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1100
1382
|
* be constructed directly with a preset name without going through
|
|
1101
1383
|
* `Markdown`.
|
|
1102
1384
|
*/
|
|
1103
|
-
constructor(code, lang, maxWidth, theme, selectable = true) {
|
|
1385
|
+
constructor(code, lang, maxWidth, theme, selectable = true, options = {}) {
|
|
1104
1386
|
super();
|
|
1105
1387
|
const resolved = resolvePresetTheme(theme);
|
|
1106
1388
|
this.source = code;
|
|
@@ -1110,15 +1392,129 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1110
1392
|
this.pad = resolved.codePadding;
|
|
1111
1393
|
this.codeFont = `${resolved.codeFontSize}px ${resolved.codeFont}`;
|
|
1112
1394
|
this.selectable = selectable;
|
|
1395
|
+
this.langFont = `${resolved.codeLangFontSize}px ${resolved.codeFont}`;
|
|
1396
|
+
this.showLanguage = options.showLanguage === true && this.languageLabel() !== "";
|
|
1113
1397
|
this.lines = [];
|
|
1114
1398
|
this.width = maxWidth;
|
|
1115
1399
|
this.buildLines(code);
|
|
1400
|
+
this.on("wheel", (e) => {
|
|
1401
|
+
const max = this.maxScrollX;
|
|
1402
|
+
if (max <= 0) return;
|
|
1403
|
+
if (e.ctrlKey === true) return;
|
|
1404
|
+
const deltaMode = e.deltaMode ?? 0;
|
|
1405
|
+
let deltaX = e.deltaX ?? 0;
|
|
1406
|
+
let deltaY = e.deltaY ?? 0;
|
|
1407
|
+
if (deltaMode === 1) {
|
|
1408
|
+
deltaX *= 16;
|
|
1409
|
+
deltaY *= 16;
|
|
1410
|
+
} else if (deltaMode === 2) {
|
|
1411
|
+
deltaX *= this.width;
|
|
1412
|
+
deltaY *= this.height;
|
|
1413
|
+
}
|
|
1414
|
+
const horizontal = e.shiftKey === true ? deltaY || deltaX : deltaX;
|
|
1415
|
+
if (horizontal === 0) return;
|
|
1416
|
+
const before = this.scrollX;
|
|
1417
|
+
this.setScrollX(before + horizontal);
|
|
1418
|
+
if (this.scrollX !== before) e.nativeEvent?.preventDefault?.();
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* The language name shown in the header, or `''` when there is nothing to show.
|
|
1423
|
+
*
|
|
1424
|
+
* Normalized exactly as the highlighter normalizes its lookup key, so the label
|
|
1425
|
+
* and the colouring can never disagree about which language this is: a fence
|
|
1426
|
+
* may be written ` ```Bash ` or carry attributes (` ```ts title="a.ts" `), and
|
|
1427
|
+
* the label has to be the language, not the raw info string.
|
|
1428
|
+
*
|
|
1429
|
+
* Lowercased for the same reason `streamdown` lowercases its own
|
|
1430
|
+
* (`lib/code-block/header.tsx:15`): the fence's capitalization is incidental,
|
|
1431
|
+
* and a document mixing ` ```JS ` with ` ```js ` should not render two
|
|
1432
|
+
* different-looking labels for one language.
|
|
1433
|
+
*/
|
|
1434
|
+
languageLabel() {
|
|
1435
|
+
return this.lang.trim().toLowerCase().split(/[\s:,{]/)[0] ?? "";
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Height in px of the header band, or `0` when it is off.
|
|
1439
|
+
*
|
|
1440
|
+
* The label sits in a band of its own rather than floating over the code,
|
|
1441
|
+
* because a translucent overlay above real glyphs is unreadable at small sizes
|
|
1442
|
+
* and would fight the horizontal scroll: the code slides under it, so any text
|
|
1443
|
+
* drawn on top would collide with a different token every frame.
|
|
1444
|
+
*/
|
|
1445
|
+
headerHeight() {
|
|
1446
|
+
if (!this.showLanguage) return 0;
|
|
1447
|
+
return this.theme.codeLangFontSize + Math.round(this.pad * 0.75);
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Local y of the first line of code.
|
|
1451
|
+
*
|
|
1452
|
+
* Everything that positions a row — the painter, the projection, the grid's
|
|
1453
|
+
* own origin — goes through this, so the header offset cannot be applied to
|
|
1454
|
+
* one and forgotten on another. That class of mismatch is exactly what
|
|
1455
|
+
* detaches selection carriers from the glyphs they cover.
|
|
1456
|
+
*/
|
|
1457
|
+
contentTop() {
|
|
1458
|
+
return this.headerHeight() + this.pad;
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Current horizontal scroll offset in local px, clamped to what the content
|
|
1462
|
+
* currently allows.
|
|
1463
|
+
*
|
|
1464
|
+
* Clamped on READ, not only on write, because `setWidth()` may shrink the box
|
|
1465
|
+
* after a scroll and is contractually forbidden from rebuilding anything. Both
|
|
1466
|
+
* the painter and the projection read through here, which is what keeps the
|
|
1467
|
+
* glyphs and the selection carriers on the same offset within a frame.
|
|
1468
|
+
*/
|
|
1469
|
+
get scrollX() {
|
|
1470
|
+
return Math.min(this.scrollXValue, this.maxScrollX);
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Widest line's overflow past the padded box, i.e. the maximum useful
|
|
1474
|
+
* {@link scrollX}. `0` when every line already fits.
|
|
1475
|
+
*/
|
|
1476
|
+
get maxScrollX() {
|
|
1477
|
+
return Math.max(0, this.contentWidth() - (this.width - this.pad * 2));
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Widest prepared line, memoized against the grid that produced it.
|
|
1481
|
+
*
|
|
1482
|
+
* Read by {@link scrollX}, which both `render()` and `getContentProjection()`
|
|
1483
|
+
* call every synced frame, so an O(lines) scan here would be an O(document) cost
|
|
1484
|
+
* per frame on a long block — the exact shape the per-line projection window
|
|
1485
|
+
* exists to avoid. The grid is rebuilt only when the content changes, so the
|
|
1486
|
+
* cache key is identity of the grid object.
|
|
1487
|
+
*/
|
|
1488
|
+
contentWidth() {
|
|
1489
|
+
const grid = this.ensureGrid();
|
|
1490
|
+
if (this.contentWidthGrid === grid) return this.contentWidthValue;
|
|
1491
|
+
let widest = 0;
|
|
1492
|
+
for (const line of grid.lines) {
|
|
1493
|
+
if (line.width > widest) widest = line.width;
|
|
1494
|
+
}
|
|
1495
|
+
this.contentWidthGrid = grid;
|
|
1496
|
+
this.contentWidthValue = widest;
|
|
1497
|
+
return widest;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Scroll horizontally to `x`, clamped to `[0, maxScrollX]`.
|
|
1501
|
+
*
|
|
1502
|
+
* @returns `this` for chaining.
|
|
1503
|
+
*/
|
|
1504
|
+
setScrollX(x) {
|
|
1505
|
+
const next = Math.max(0, Math.min(this.maxScrollX, x));
|
|
1506
|
+
if (next === this.scrollXValue) return this;
|
|
1507
|
+
this.scrollXValue = next;
|
|
1508
|
+
this.contentEpoch++;
|
|
1509
|
+
this.scene?.markDirty();
|
|
1510
|
+
return this;
|
|
1116
1511
|
}
|
|
1117
1512
|
/** Re-parse code content (e.g. for live editing). */
|
|
1118
1513
|
setCode(code, lang) {
|
|
1119
1514
|
if (lang !== void 0) this.lang = lang;
|
|
1120
1515
|
this.source = code;
|
|
1121
1516
|
this.buildLines(code);
|
|
1517
|
+
this.scrollXValue = Math.min(this.scrollXValue, this.maxScrollX);
|
|
1122
1518
|
this.scene?.markDirty();
|
|
1123
1519
|
return this;
|
|
1124
1520
|
}
|
|
@@ -1138,9 +1534,13 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1138
1534
|
* Deliberately does **not** rebuild the grid or the highlight, because code does
|
|
1139
1535
|
* not reflow: lines are placed on a fixed monospace grid at `col × cellWidth` and
|
|
1140
1536
|
* a long line overflows rather than wrapping, so `height` is a function of line
|
|
1141
|
-
* *count* alone. The width
|
|
1142
|
-
* change the glyph geometry — the source, the language, the font —
|
|
1143
|
-
* {@link setCode} and invalidates the grid there.
|
|
1537
|
+
* *count* alone. The width sizes the rounded background and the clip. Anything
|
|
1538
|
+
* that would change the glyph geometry — the source, the language, the font —
|
|
1539
|
+
* goes through {@link setCode} and invalidates the grid there.
|
|
1540
|
+
*
|
|
1541
|
+
* A narrower box can leave {@link scrollX} past the new end of travel. That is
|
|
1542
|
+
* resolved by clamping on read rather than by adjusting anything here, so this
|
|
1543
|
+
* method keeps costing nothing.
|
|
1144
1544
|
*
|
|
1145
1545
|
* @returns `this` for chaining.
|
|
1146
1546
|
*/
|
|
@@ -1154,16 +1554,21 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1154
1554
|
getContentProjection(hint) {
|
|
1155
1555
|
if (!this.source) return null;
|
|
1156
1556
|
const grid = this.ensureGrid();
|
|
1557
|
+
const scrollX = this.scrollX;
|
|
1157
1558
|
const rows = [];
|
|
1158
1559
|
rows.length = grid.lines.length;
|
|
1159
1560
|
for (let row = 0; row < grid.lines.length; row++) {
|
|
1160
1561
|
const line = grid.lines[row];
|
|
1161
|
-
const y = this.
|
|
1562
|
+
const y = this.contentTop() + row * this.lineH;
|
|
1162
1563
|
if (!(0, import_core2.contentLineInHint)(hint, y, this.lineH)) continue;
|
|
1163
1564
|
rows[row] = {
|
|
1164
1565
|
text: this.source.slice(line.sourceStart, line.sourceEnd),
|
|
1165
1566
|
separatorAfter: this.source.slice(line.sourceEnd, line.nextSourceStart) || void 0,
|
|
1166
|
-
|
|
1567
|
+
// The SAME offset `render()` subtracts, read through the same clamping
|
|
1568
|
+
// accessor. Cell carriers are `position: relative` inside this `absolute`
|
|
1569
|
+
// line box, so shifting the line's x translates every cell of the line
|
|
1570
|
+
// rigidly and selection stays over the glyphs.
|
|
1571
|
+
x: this.pad - scrollX,
|
|
1167
1572
|
y,
|
|
1168
1573
|
baseline: this.lineH * 0.75,
|
|
1169
1574
|
font: this.codeFont,
|
|
@@ -1183,6 +1588,12 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1183
1588
|
// render() draws cell-by-cell (no ligatures can form); the DOM copy
|
|
1184
1589
|
// must not ligate either or Firefox selection geometry drifts.
|
|
1185
1590
|
ligatures: "none",
|
|
1591
|
+
// `render()` clips the glyph pass to this box, so the DOM copy must too.
|
|
1592
|
+
// A line wider than the box otherwise projects carriers past the entity,
|
|
1593
|
+
// and the browser paints their selection highlight over whatever is drawn
|
|
1594
|
+
// beside the block — measured 1580px of carrier against a 1566px viewport
|
|
1595
|
+
// on a real page, the highlight running through the prose to its right.
|
|
1596
|
+
clipToBounds: true,
|
|
1186
1597
|
grid
|
|
1187
1598
|
};
|
|
1188
1599
|
}
|
|
@@ -1197,6 +1608,11 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1197
1608
|
*
|
|
1198
1609
|
* The last previously-seen line is deliberately NOT reused: a chunk usually
|
|
1199
1610
|
* lands mid-line, so that line's text (and therefore its tokenization) changes.
|
|
1611
|
+
*
|
|
1612
|
+
* Prefix reuse survives multi-line constructs because {@link lineCarry} records
|
|
1613
|
+
* the state ENTERING each line, so resuming at the reuse boundary needs no
|
|
1614
|
+
* rescan: a carried state is a pure function of the preceding text, and that
|
|
1615
|
+
* text is byte-identical over the reused prefix by construction.
|
|
1200
1616
|
*/
|
|
1201
1617
|
buildLines(code) {
|
|
1202
1618
|
this.contentEpoch++;
|
|
@@ -1207,18 +1623,20 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1207
1623
|
const limit = Math.min(previous.length - 1, rawLines.length);
|
|
1208
1624
|
while (reusable < limit && previous[reusable] === rawLines[reusable]) reusable++;
|
|
1209
1625
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
this.
|
|
1216
|
-
|
|
1217
|
-
|
|
1626
|
+
const lines = reusable > 0 ? this.lines.slice(0, reusable) : [];
|
|
1627
|
+
const carries = reusable > 0 ? this.lineCarry.slice(0, reusable) : [];
|
|
1628
|
+
let carry = reusable > 0 ? this.lineCarry[reusable] ?? null : null;
|
|
1629
|
+
for (let i = reusable; i < rawLines.length; i++) {
|
|
1630
|
+
carries.push(carry);
|
|
1631
|
+
const result = highlightLine(rawLines[i], this.lang, this.theme, carry);
|
|
1632
|
+
lines.push(result.segments);
|
|
1633
|
+
carry = result.carry;
|
|
1218
1634
|
}
|
|
1635
|
+
this.lines = lines;
|
|
1636
|
+
this.lineCarry = carries;
|
|
1219
1637
|
this.rawLines = rawLines;
|
|
1220
1638
|
this.grid = null;
|
|
1221
|
-
this.height = this.
|
|
1639
|
+
this.height = this.contentTop() + this.pad + rawLines.length * this.lineH;
|
|
1222
1640
|
}
|
|
1223
1641
|
ensureGrid() {
|
|
1224
1642
|
const cellWidth = this.cellWidth || Math.max(1, (0, import_ui.measureText)("M", this.codeFont));
|
|
@@ -1232,7 +1650,15 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1232
1650
|
}
|
|
1233
1651
|
return this.grid;
|
|
1234
1652
|
}
|
|
1235
|
-
/**
|
|
1653
|
+
/**
|
|
1654
|
+
* Not hit-testable, and deliberately still not `interactive`, even though the
|
|
1655
|
+
* block now consumes wheel events to scroll.
|
|
1656
|
+
*
|
|
1657
|
+
* The wheel arrives from the content-projection div rather than from canvas
|
|
1658
|
+
* hit-testing, so no a11y shadow node is needed. Creating one would place a
|
|
1659
|
+
* `pointer-events: auto` element above the transparent text mirror and swallow
|
|
1660
|
+
* the mousedown that starts a native drag-selection.
|
|
1661
|
+
*/
|
|
1236
1662
|
isPointInside() {
|
|
1237
1663
|
return false;
|
|
1238
1664
|
}
|
|
@@ -1244,8 +1670,25 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1244
1670
|
const atlas = codeGlyphAtlas(r);
|
|
1245
1671
|
const atlasSource = atlas?.source ?? null;
|
|
1246
1672
|
const blit = atlas ? r.drawImageRect : void 0;
|
|
1673
|
+
const header = this.headerHeight();
|
|
1674
|
+
if (header > 0) {
|
|
1675
|
+
r.fillText(
|
|
1676
|
+
this.languageLabel(),
|
|
1677
|
+
this.pad,
|
|
1678
|
+
// Vertically centred in the band by its own cap height rather than by
|
|
1679
|
+
// font size: `fillText` takes a baseline, so centring the em box would
|
|
1680
|
+
// sit the visible letterforms low. 0.7 of the label size below the band's
|
|
1681
|
+
// centre line is where a lowercase-plus-cap run reads as centred.
|
|
1682
|
+
(header + this.theme.codeLangFontSize * 0.7) / 2,
|
|
1683
|
+
this.langFont,
|
|
1684
|
+
this.theme.codeLangColor
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
r.save();
|
|
1688
|
+
r.clip(0, header, this.width, this.height - header);
|
|
1689
|
+
const scrollX = this.scrollX;
|
|
1247
1690
|
for (let row = 0; row < grid.lines.length; row++) {
|
|
1248
|
-
const yBaseline = this.
|
|
1691
|
+
const yBaseline = this.contentTop() + row * this.lineH + this.lineH * 0.75;
|
|
1249
1692
|
const segments = this.lines[row];
|
|
1250
1693
|
let segmentIndex = 0;
|
|
1251
1694
|
let segmentEnd = segments[0]?.text.length ?? 0;
|
|
@@ -1259,7 +1702,8 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1259
1702
|
const sourceText = this.source.slice(cell.sourceStart, cell.sourceEnd);
|
|
1260
1703
|
if (cell.advance <= 0 || sourceText === " " || sourceText === " ") continue;
|
|
1261
1704
|
const color = segments[segmentIndex]?.color ?? this.theme.codeColor;
|
|
1262
|
-
const x = this.pad + cell.x;
|
|
1705
|
+
const x = this.pad + cell.x - scrollX;
|
|
1706
|
+
if (x + cell.advance < 0 || x > this.width) continue;
|
|
1263
1707
|
if (blit && atlas) {
|
|
1264
1708
|
const slot = atlas.get(this.codeFont, color, cell.glyph);
|
|
1265
1709
|
const src = atlasSource ?? atlas.source;
|
|
@@ -1282,6 +1726,7 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1282
1726
|
r.fillText(cell.glyph, x, yBaseline, this.codeFont, color);
|
|
1283
1727
|
}
|
|
1284
1728
|
}
|
|
1729
|
+
r.restore();
|
|
1285
1730
|
}
|
|
1286
1731
|
};
|
|
1287
1732
|
var codeAtlases = /* @__PURE__ */ new Map();
|
|
@@ -2533,6 +2978,20 @@ function defaultSaveFile(filename, content, mimeType) {
|
|
|
2533
2978
|
doc.body.removeChild(anchor);
|
|
2534
2979
|
URL.revokeObjectURL(url);
|
|
2535
2980
|
}
|
|
2981
|
+
function resolveBlockAffordanceConfig(config = {}) {
|
|
2982
|
+
return {
|
|
2983
|
+
copy: config.copy ?? true,
|
|
2984
|
+
download: config.download ?? true,
|
|
2985
|
+
labels: {
|
|
2986
|
+
copyCode: config.labels?.copyCode ?? "Copy code",
|
|
2987
|
+
downloadCode: config.labels?.downloadCode ?? "Download code",
|
|
2988
|
+
copyTable: config.labels?.copyTable ?? "Copy table",
|
|
2989
|
+
downloadTable: config.labels?.downloadTable ?? "Download table",
|
|
2990
|
+
copied: config.labels?.copied ?? "Copied",
|
|
2991
|
+
saved: config.labels?.saved ?? "Saved"
|
|
2992
|
+
}
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2536
2995
|
var BlockAffordanceButton = class _BlockAffordanceButton extends import_ui3.Button {
|
|
2537
2996
|
constructor(label, successLabel, act, opts = {}) {
|
|
2538
2997
|
super(label, { ...opts, onClick: () => this.run() });
|
|
@@ -2787,6 +3246,12 @@ import_marked.marked.use({
|
|
|
2787
3246
|
}
|
|
2788
3247
|
]
|
|
2789
3248
|
});
|
|
3249
|
+
var MISWIRED_LAYOUT_HOOK_NAMES = [
|
|
3250
|
+
"onHeightChanged",
|
|
3251
|
+
"onHeightChange",
|
|
3252
|
+
"onLayoutUpdate",
|
|
3253
|
+
"onResize"
|
|
3254
|
+
];
|
|
2790
3255
|
var markdownWorker = null;
|
|
2791
3256
|
var workerIdCounter = 0;
|
|
2792
3257
|
var workerInstanceCounter = 0;
|
|
@@ -2848,20 +3313,62 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
2848
3313
|
* affordance.
|
|
2849
3314
|
*/
|
|
2850
3315
|
blockAffordances;
|
|
3316
|
+
/**
|
|
3317
|
+
* Which controls a block carries and what they are called, with defaults
|
|
3318
|
+
* applied.
|
|
3319
|
+
*
|
|
3320
|
+
* Resolved once in the constructor rather than per block: the defaults are
|
|
3321
|
+
* fixed, and re-deriving them for every code fence in a long document would
|
|
3322
|
+
* repeat the same six `??` fallbacks for no benefit.
|
|
3323
|
+
*/
|
|
3324
|
+
affordanceConfig;
|
|
3325
|
+
/**
|
|
3326
|
+
* Whether code blocks show their language in a header band.
|
|
3327
|
+
*
|
|
3328
|
+
* Read when a block entity is built, exactly like {@link blockAffordances}, so
|
|
3329
|
+
* it affects blocks rendered from here on rather than retroactively.
|
|
3330
|
+
*/
|
|
3331
|
+
showCodeLanguage;
|
|
2851
3332
|
/** Clipboard writer used by the copy controls. */
|
|
2852
3333
|
writeClipboard;
|
|
2853
3334
|
/** File saver used by the download controls. */
|
|
2854
3335
|
saveFile;
|
|
2855
3336
|
activeBlockMetrics = null;
|
|
2856
3337
|
/**
|
|
2857
|
-
* Called after
|
|
3338
|
+
* Called after this entity's own `width`/`height` changed because the document
|
|
3339
|
+
* was re-laid-out. **This is the hook to wire up when a host has to move or
|
|
3340
|
+
* resize anything positioned below the document.**
|
|
3341
|
+
*
|
|
3342
|
+
* Fires from three paths, all of which republish `width`/`height` from
|
|
3343
|
+
* `content`:
|
|
3344
|
+
*
|
|
3345
|
+
* - a streamed append (`updateTokens`),
|
|
3346
|
+
* - a width change ({@link setMaxWidth}),
|
|
3347
|
+
* - a paragraph image whose decoded bitmap corrected the guessed aspect ratio
|
|
3348
|
+
* (`reflowAfterImageResize`) — the guess is a flat 16:10, so this one fires
|
|
3349
|
+
* on essentially every document containing an image, and a host that misses
|
|
3350
|
+
* it leaves every block below the image overlapping it.
|
|
3351
|
+
*
|
|
3352
|
+
* It does **not** fire from `setContent()`, which replaces the whole document
|
|
3353
|
+
* and is a call the host already made, so this is a re-layout signal rather
|
|
3354
|
+
* than a complete size signal.
|
|
3355
|
+
*
|
|
3356
|
+
* A `VirtualList` needs none of this to track a streaming row: it re-reads
|
|
3357
|
+
* `height` on every mounted row each frame, so it sees this entity grow without
|
|
3358
|
+
* being told. Prefer that where it applies. Reach for this callback when the
|
|
3359
|
+
* host owns absolute positions of its own — a page that stacks navigation, a
|
|
3360
|
+
* footer and a scroll height under the document has to recompute them here.
|
|
2858
3361
|
*
|
|
2859
|
-
*
|
|
2860
|
-
*
|
|
2861
|
-
*
|
|
2862
|
-
* path only, **not** from `setContent()`, so it is not a complete size signal.
|
|
3362
|
+
* There is no `onHeightChanged`. That name has been assigned by real callers
|
|
3363
|
+
* through an `as unknown as` cast, which compiles, silences the type error and
|
|
3364
|
+
* then never fires; if a layout callback appears dead, check the name first.
|
|
2863
3365
|
*/
|
|
2864
3366
|
onLayoutUpdated;
|
|
3367
|
+
/**
|
|
3368
|
+
* Latch for the miswired-hook warning, so a streaming document warns once
|
|
3369
|
+
* rather than on every chunk.
|
|
3370
|
+
*/
|
|
3371
|
+
hasWarnedLayoutHookName = false;
|
|
2865
3372
|
/**
|
|
2866
3373
|
* The document's BODY text — everything after any front matter block.
|
|
2867
3374
|
*
|
|
@@ -3104,6 +3611,8 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
3104
3611
|
this.selectable = opts.selectable ?? true;
|
|
3105
3612
|
this._userTiming = opts.userTiming ?? false;
|
|
3106
3613
|
this.blockAffordances = opts.blockAffordances ?? false;
|
|
3614
|
+
this.affordanceConfig = resolveBlockAffordanceConfig(opts.affordances);
|
|
3615
|
+
this.showCodeLanguage = opts.showCodeLanguage ?? false;
|
|
3107
3616
|
this.writeClipboard = opts.writeClipboard ?? defaultWriteClipboard;
|
|
3108
3617
|
this.saveFile = opts.saveFile ?? defaultSaveFile;
|
|
3109
3618
|
this.content = new import_ui4.Stack({
|
|
@@ -3312,7 +3821,7 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
3312
3821
|
this.content.layout();
|
|
3313
3822
|
this.width = this.content.width;
|
|
3314
3823
|
this.height = this.content.height;
|
|
3315
|
-
this.
|
|
3824
|
+
this.notifyLayoutUpdated();
|
|
3316
3825
|
this.scene?.markDirty();
|
|
3317
3826
|
return this;
|
|
3318
3827
|
}
|
|
@@ -4065,40 +4574,60 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
4065
4574
|
const controls = make();
|
|
4066
4575
|
return controls.length > 0 ? new BlockWithAffordances(block, controls) : block;
|
|
4067
4576
|
}
|
|
4068
|
-
/** Copy and download controls for one fenced code block. */
|
|
4577
|
+
/** Copy and download controls for one fenced code block, per {@link affordanceConfig}. */
|
|
4069
4578
|
codeBlockAffordances(source, lang) {
|
|
4070
4579
|
const opts = this.affordanceButtonOptions();
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4580
|
+
const { copy, download, labels } = this.affordanceConfig;
|
|
4581
|
+
const controls = [];
|
|
4582
|
+
if (copy) {
|
|
4583
|
+
controls.push(
|
|
4584
|
+
new BlockAffordanceButton(
|
|
4585
|
+
labels.copyCode,
|
|
4586
|
+
labels.copied,
|
|
4587
|
+
() => this.writeClipboard(source),
|
|
4588
|
+
opts
|
|
4589
|
+
)
|
|
4590
|
+
);
|
|
4591
|
+
}
|
|
4592
|
+
if (download) {
|
|
4593
|
+
controls.push(
|
|
4594
|
+
new BlockAffordanceButton(
|
|
4595
|
+
labels.downloadCode,
|
|
4596
|
+
labels.saved,
|
|
4597
|
+
() => this.saveFile(`code.${extensionForLanguage(lang)}`, source, mimeForLanguage(lang)),
|
|
4598
|
+
opts
|
|
4599
|
+
)
|
|
4600
|
+
);
|
|
4601
|
+
}
|
|
4602
|
+
return controls;
|
|
4603
|
+
}
|
|
4604
|
+
/** Copy (as Markdown) and download (as CSV) controls for one table, per {@link affordanceConfig}. */
|
|
4082
4605
|
tableAffordances(tblToken) {
|
|
4606
|
+
const { copy, download, labels } = this.affordanceConfig;
|
|
4083
4607
|
const content = tableContentOf(tblToken);
|
|
4084
4608
|
const opts = this.affordanceButtonOptions();
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
)
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4609
|
+
const controls = [];
|
|
4610
|
+
if (copy) {
|
|
4611
|
+
controls.push(
|
|
4612
|
+
new BlockAffordanceButton(
|
|
4613
|
+
labels.copyTable,
|
|
4614
|
+
labels.copied,
|
|
4615
|
+
() => this.writeClipboard(tableToMarkdown(content)),
|
|
4616
|
+
opts
|
|
4617
|
+
)
|
|
4618
|
+
);
|
|
4619
|
+
}
|
|
4620
|
+
if (download) {
|
|
4621
|
+
controls.push(
|
|
4622
|
+
new BlockAffordanceButton(
|
|
4623
|
+
labels.downloadTable,
|
|
4624
|
+
labels.saved,
|
|
4625
|
+
() => this.saveFile("table.csv", tableToCsv(content), "text/csv;charset=utf-8"),
|
|
4626
|
+
opts
|
|
4627
|
+
)
|
|
4628
|
+
);
|
|
4629
|
+
}
|
|
4630
|
+
return controls;
|
|
4102
4631
|
}
|
|
4103
4632
|
/**
|
|
4104
4633
|
* Button styling for the affordances, derived from the document theme.
|
|
@@ -4182,7 +4711,40 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
4182
4711
|
}
|
|
4183
4712
|
this.width = this.content.width;
|
|
4184
4713
|
this.height = this.content.height;
|
|
4185
|
-
this.
|
|
4714
|
+
this.notifyLayoutUpdated();
|
|
4715
|
+
}
|
|
4716
|
+
/**
|
|
4717
|
+
* Publish a completed re-layout to the host.
|
|
4718
|
+
*
|
|
4719
|
+
* Every path that republishes `width`/`height` from `content` ends here rather
|
|
4720
|
+
* than calling {@link onLayoutUpdated} directly, so the misuse check below is
|
|
4721
|
+
* reached however the re-layout was triggered.
|
|
4722
|
+
*
|
|
4723
|
+
* The check exists because the failure it catches is silent and was found in
|
|
4724
|
+
* production, not in review. A host wired its reflow to `onHeightChanged` — a
|
|
4725
|
+
* name this class has never had — through an `as unknown as` cast. The cast
|
|
4726
|
+
* satisfied the compiler, the callback never fired, and every post containing
|
|
4727
|
+
* an image stayed laid out against the guessed 16:10 aspect ratio with a stale
|
|
4728
|
+
* document scroll height. Nothing in the type system, the tests or the console
|
|
4729
|
+
* said anything. A property that is *only ever assigned* has no read site to
|
|
4730
|
+
* fail, so the one place that can notice is the moment we would have called it.
|
|
4731
|
+
*/
|
|
4732
|
+
notifyLayoutUpdated() {
|
|
4733
|
+
if (this.onLayoutUpdated) {
|
|
4734
|
+
this.onLayoutUpdated();
|
|
4735
|
+
return;
|
|
4736
|
+
}
|
|
4737
|
+
if (!this.hasWarnedLayoutHookName) {
|
|
4738
|
+
const wrongName = MISWIRED_LAYOUT_HOOK_NAMES.find(
|
|
4739
|
+
(name) => typeof this[name] === "function"
|
|
4740
|
+
);
|
|
4741
|
+
if (wrongName) {
|
|
4742
|
+
this.hasWarnedLayoutHookName = true;
|
|
4743
|
+
console.warn(
|
|
4744
|
+
`[VectoJS] Markdown.${wrongName} is not a VectoJS callback and will never fire. The document just re-laid-out and nothing was notified. Assign \`onLayoutUpdated\` instead \u2014 anything positioned below the document needs it, and a paragraph image corrects its guessed aspect ratio on decode, so a missed signal leaves following blocks overlapping the image.`
|
|
4745
|
+
);
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4186
4748
|
}
|
|
4187
4749
|
/**
|
|
4188
4750
|
* Re-derive one `MarkdownContainer`'s cached box from its children.
|
|
@@ -4993,9 +5555,7 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
4993
5555
|
this.width = this.content.width;
|
|
4994
5556
|
this.height = this.content.height;
|
|
4995
5557
|
this.scene?.markDirty();
|
|
4996
|
-
|
|
4997
|
-
this.onLayoutUpdated();
|
|
4998
|
-
}
|
|
5558
|
+
this.notifyLayoutUpdated();
|
|
4999
5559
|
}
|
|
5000
5560
|
/**
|
|
5001
5561
|
* Render one nested block with a temporary width/margin context while
|
|
@@ -5211,7 +5771,9 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
5211
5771
|
}
|
|
5212
5772
|
}
|
|
5213
5773
|
return this.withBlockAffordances(
|
|
5214
|
-
new CodeBlock(codeToken.text, lang, availableWidth, t, this.selectable
|
|
5774
|
+
new CodeBlock(codeToken.text, lang, availableWidth, t, this.selectable, {
|
|
5775
|
+
showLanguage: this.showCodeLanguage
|
|
5776
|
+
}),
|
|
5215
5777
|
() => this.codeBlockAffordances(codeToken.text, lang)
|
|
5216
5778
|
);
|
|
5217
5779
|
}
|
|
@@ -5443,6 +6005,7 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
5443
6005
|
extensionForLanguage,
|
|
5444
6006
|
footnoteMarker,
|
|
5445
6007
|
hasFencedBlockRenderer,
|
|
6008
|
+
highlightedLanguages,
|
|
5446
6009
|
isFencedBlockRendererReady,
|
|
5447
6010
|
isMathJaxReady,
|
|
5448
6011
|
isPresetName,
|
|
@@ -5451,6 +6014,7 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
5451
6014
|
preloadMathJax,
|
|
5452
6015
|
registerFencedBlockRenderer,
|
|
5453
6016
|
renderFencedBlock,
|
|
6017
|
+
resolveBlockAffordanceConfig,
|
|
5454
6018
|
resolvePresetTheme,
|
|
5455
6019
|
scanFrontMatter,
|
|
5456
6020
|
tableContentOf,
|