quikchat 1.2.4 → 1.2.6
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/README.md +2 -2
- package/dist/build-manifest.json +73 -80
- package/dist/quikchat-md-full.cjs.js +536 -255
- package/dist/quikchat-md-full.cjs.js.map +1 -1
- package/dist/quikchat-md-full.cjs.min.js +3 -3
- package/dist/quikchat-md-full.cjs.min.js.map +1 -1
- package/dist/quikchat-md-full.esm.js +536 -255
- package/dist/quikchat-md-full.esm.js.map +1 -1
- package/dist/quikchat-md-full.esm.min.js +3 -3
- package/dist/quikchat-md-full.esm.min.js.map +1 -1
- package/dist/quikchat-md-full.umd.js +536 -255
- package/dist/quikchat-md-full.umd.js.map +1 -1
- package/dist/quikchat-md-full.umd.min.js +3 -3
- package/dist/quikchat-md-full.umd.min.js.map +1 -1
- package/dist/quikchat-md.cjs.js +520 -245
- package/dist/quikchat-md.cjs.js.map +1 -1
- package/dist/quikchat-md.cjs.min.js +3 -3
- package/dist/quikchat-md.cjs.min.js.map +1 -1
- package/dist/quikchat-md.esm.js +520 -245
- package/dist/quikchat-md.esm.js.map +1 -1
- package/dist/quikchat-md.esm.min.js +3 -3
- package/dist/quikchat-md.esm.min.js.map +1 -1
- package/dist/quikchat-md.umd.js +520 -245
- package/dist/quikchat-md.umd.js.map +1 -1
- package/dist/quikchat-md.umd.min.js +3 -3
- package/dist/quikchat-md.umd.min.js.map +1 -1
- package/dist/quikchat.cjs.js +2 -2
- package/dist/quikchat.cjs.js.map +1 -1
- package/dist/quikchat.cjs.min.js +1 -1
- package/dist/quikchat.cjs.min.js.map +1 -1
- package/dist/quikchat.css +351 -120
- package/dist/quikchat.esm.js +2 -2
- package/dist/quikchat.esm.js.map +1 -1
- package/dist/quikchat.esm.min.js +1 -1
- package/dist/quikchat.esm.min.js.map +1 -1
- package/dist/quikchat.min.css +1 -1
- package/dist/quikchat.umd.js +2 -2
- package/dist/quikchat.umd.js.map +1 -1
- package/dist/quikchat.umd.min.js +1 -1
- package/dist/quikchat.umd.min.js.map +1 -1
- package/package.json +6 -5
|
@@ -600,7 +600,7 @@
|
|
|
600
600
|
value: function _autoGrowTextarea() {
|
|
601
601
|
var el = this._textEntry;
|
|
602
602
|
el.style.height = 'auto';
|
|
603
|
-
var maxHeight =
|
|
603
|
+
var maxHeight = 120;
|
|
604
604
|
el.style.height = Math.min(el.scrollHeight, maxHeight) + 'px';
|
|
605
605
|
el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';
|
|
606
606
|
}
|
|
@@ -1068,7 +1068,7 @@
|
|
|
1068
1068
|
key: "version",
|
|
1069
1069
|
value: function version() {
|
|
1070
1070
|
return {
|
|
1071
|
-
"version": "1.2.
|
|
1071
|
+
"version": "1.2.6",
|
|
1072
1072
|
"license": "BSD-2",
|
|
1073
1073
|
"url": "https://github/deftio/quikchat"
|
|
1074
1074
|
};
|
|
@@ -1128,35 +1128,140 @@
|
|
|
1128
1128
|
|
|
1129
1129
|
/**
|
|
1130
1130
|
* quikdown_bd - Bidirectional Markdown Parser
|
|
1131
|
-
* @version 1.
|
|
1131
|
+
* @version 1.2.10
|
|
1132
1132
|
* @license BSD-2-Clause
|
|
1133
1133
|
* @copyright DeftIO 2025
|
|
1134
1134
|
*/
|
|
1135
1135
|
/**
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1136
|
+
* quikdown_classify — Shared line-classification utilities
|
|
1137
|
+
* ═════════════════════════════════════════════════════════
|
|
1138
|
+
*
|
|
1139
|
+
* Pure functions for classifying markdown lines. Used by both the main
|
|
1140
|
+
* parser (quikdown.js) and the editor (quikdown_edit.js) so the logic
|
|
1141
|
+
* lives in one place.
|
|
1142
|
+
*
|
|
1143
|
+
* All functions operate on a **trimmed** line (caller must trim).
|
|
1144
|
+
* None use regexes with nested quantifiers — every check is either a
|
|
1145
|
+
* simple regex or a linear scan, so there is zero ReDoS risk.
|
|
1146
1146
|
*/
|
|
1147
1147
|
|
|
1148
|
-
// Version will be injected at build time
|
|
1149
|
-
const quikdownVersion = '1.1.1';
|
|
1150
1148
|
|
|
1151
|
-
|
|
1149
|
+
/**
|
|
1150
|
+
* Dash-only HR check — exact parity with the main parser's original
|
|
1151
|
+
* regex `/^---+\s*$/`. Only matches lines of three or more dashes
|
|
1152
|
+
* with optional trailing whitespace (no interspersed spaces).
|
|
1153
|
+
*
|
|
1154
|
+
* @param {string} trimmed The line, already trimmed
|
|
1155
|
+
* @returns {boolean}
|
|
1156
|
+
*/
|
|
1157
|
+
function isDashHRLine(trimmed) {
|
|
1158
|
+
if (trimmed.length < 3) return false;
|
|
1159
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
1160
|
+
const ch = trimmed[i];
|
|
1161
|
+
if (ch === '-') continue;
|
|
1162
|
+
// Allow trailing whitespace only
|
|
1163
|
+
if (ch === ' ' || ch === '\t') {
|
|
1164
|
+
for (let j = i + 1; j < trimmed.length; j++) {
|
|
1165
|
+
if (trimmed[j] !== ' ' && trimmed[j] !== '\t') return false;
|
|
1166
|
+
}
|
|
1167
|
+
return i >= 3; // at least 3 dashes before whitespace
|
|
1168
|
+
}
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
return true; // all dashes
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* quikdown — A compact, scanner-based markdown parser
|
|
1176
|
+
* ════════════════════════════════════════════════════
|
|
1177
|
+
*
|
|
1178
|
+
* Architecture overview (v1.2.8 — lexer rewrite)
|
|
1179
|
+
* ───────────────────────────────────────────────
|
|
1180
|
+
* Prior to v1.2.8, quikdown used a multi-pass regex pipeline: each block
|
|
1181
|
+
* type (headings, blockquotes, HR, lists, tables) and each inline format
|
|
1182
|
+
* (bold, italic, links, …) was handled by its own global regex applied
|
|
1183
|
+
* sequentially to the full document string. That worked but made the code
|
|
1184
|
+
* hard to extend and debug — a new construct meant adding another regex
|
|
1185
|
+
* pass, and ordering bugs between passes were subtle.
|
|
1186
|
+
*
|
|
1187
|
+
* Starting in v1.2.8 the parser uses a **line-scanning** approach for
|
|
1188
|
+
* block detection and a **per-block inline pass** for formatting:
|
|
1189
|
+
*
|
|
1190
|
+
* ┌─────────────────────────────────────────────────────────┐
|
|
1191
|
+
* │ Phase 1 — Code Extraction │
|
|
1192
|
+
* │ Scan for fenced code blocks (``` / ~~~) and inline │
|
|
1193
|
+
* │ code spans (`…`). Replace with §CB§ / §IC§ place- │
|
|
1194
|
+
* │ holders so code content is never touched by later │
|
|
1195
|
+
* │ phases. │
|
|
1196
|
+
* ├─────────────────────────────────────────────────────────┤
|
|
1197
|
+
* │ Phase 2 — HTML Escaping │
|
|
1198
|
+
* │ Escape &, <, >, ", ' in the remaining text to prevent │
|
|
1199
|
+
* │ XSS. (Skipped when allow_unsafe_html is true.) │
|
|
1200
|
+
* ├─────────────────────────────────────────────────────────┤
|
|
1201
|
+
* │ Phase 3 — Block Scanning │
|
|
1202
|
+
* │ Walk the text **line by line**. At each line, the │
|
|
1203
|
+
* │ scanner checks (in order): │
|
|
1204
|
+
* │ • table rows (|) │
|
|
1205
|
+
* │ • headings (#) │
|
|
1206
|
+
* │ • HR (---) │
|
|
1207
|
+
* │ • blockquotes (>) │
|
|
1208
|
+
* │ • list items (-, *, +, 1.) │
|
|
1209
|
+
* │ • code-block placeholder (§CB…§) │
|
|
1210
|
+
* │ • paragraph text (everything else) │
|
|
1211
|
+
* │ │
|
|
1212
|
+
* │ Block text is run through the **inline formatter** │
|
|
1213
|
+
* │ which handles bold, italic, strikethrough, links, │
|
|
1214
|
+
* │ images, and autolinks. │
|
|
1215
|
+
* │ │
|
|
1216
|
+
* │ Paragraphs are wrapped in <p> tags. Lazy linefeeds │
|
|
1217
|
+
* │ (single \n → <br>) are handled here too. │
|
|
1218
|
+
* ├─────────────────────────────────────────────────────────┤
|
|
1219
|
+
* │ Phase 4 — Code Restoration │
|
|
1220
|
+
* │ Replace §CB§ / §IC§ placeholders with rendered <pre> │
|
|
1221
|
+
* │ / <code> HTML, applying the fence_plugin if present. │
|
|
1222
|
+
* └─────────────────────────────────────────────────────────┘
|
|
1223
|
+
*
|
|
1224
|
+
* Why this design?
|
|
1225
|
+
* • Single pass over lines for block identification — no re-scanning.
|
|
1226
|
+
* • Each block type is a clearly separated branch, easy to add new ones.
|
|
1227
|
+
* • Inline formatting is confined to block text — can't accidentally
|
|
1228
|
+
* match across block boundaries or inside HTML tags.
|
|
1229
|
+
* • Code extraction still uses a simple regex (it's one pattern, not a
|
|
1230
|
+
* chain) because the §-placeholder approach is proven and simple.
|
|
1231
|
+
*
|
|
1232
|
+
* @param {string} markdown The markdown source text
|
|
1233
|
+
* @param {Object} options Configuration (see below)
|
|
1234
|
+
* @returns {string} Rendered HTML
|
|
1235
|
+
*/
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1239
|
+
// Constants
|
|
1240
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1241
|
+
|
|
1242
|
+
/** Build-time version stamp (injected by tools/updateVersion) */
|
|
1243
|
+
const quikdownVersion = '1.2.10';
|
|
1244
|
+
|
|
1245
|
+
/** CSS class prefix used for all generated elements */
|
|
1152
1246
|
const CLASS_PREFIX = 'quikdown-';
|
|
1153
|
-
const PLACEHOLDER_CB = '§CB';
|
|
1154
|
-
const PLACEHOLDER_IC = '§IC';
|
|
1155
1247
|
|
|
1156
|
-
|
|
1248
|
+
/** Placeholder sigils — chosen to be extremely unlikely in real text */
|
|
1249
|
+
const PLACEHOLDER_CB = '§CB'; // fenced code blocks
|
|
1250
|
+
const PLACEHOLDER_IC = '§IC'; // inline code spans
|
|
1251
|
+
|
|
1252
|
+
/** HTML entity escape map */
|
|
1157
1253
|
const ESC_MAP = {'&':'&','<':'<','>':'>','"':'"',"'":'''};
|
|
1158
1254
|
|
|
1159
|
-
//
|
|
1255
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1256
|
+
// Style definitions
|
|
1257
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Inline styles for every element quikdown can emit.
|
|
1261
|
+
* When `inline_styles: true` these are injected as style="…" attributes.
|
|
1262
|
+
* When `inline_styles: false` (default) we use class="quikdown-<tag>"
|
|
1263
|
+
* and these same values are emitted by `quikdown.emitStyles()`.
|
|
1264
|
+
*/
|
|
1160
1265
|
const QUIKDOWN_STYLES = {
|
|
1161
1266
|
h1: 'font-size:2em;font-weight:600;margin:.67em 0;text-align:left',
|
|
1162
1267
|
h2: 'font-size:1.5em;font-weight:600;margin:.83em 0',
|
|
@@ -1179,30 +1284,41 @@
|
|
|
1179
1284
|
ul: 'margin:.5em 0;padding-left:2em',
|
|
1180
1285
|
ol: 'margin:.5em 0;padding-left:2em',
|
|
1181
1286
|
li: 'margin:.25em 0',
|
|
1182
|
-
// Task list specific styles
|
|
1183
1287
|
'task-item': 'list-style:none',
|
|
1184
1288
|
'task-checkbox': 'margin-right:.5em'
|
|
1185
1289
|
};
|
|
1186
1290
|
|
|
1187
|
-
//
|
|
1291
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1292
|
+
// Attribute factory
|
|
1293
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Creates a `getAttr(tag, additionalStyle?)` helper that returns
|
|
1297
|
+
* either a class="…" or style="…" attribute string depending on mode.
|
|
1298
|
+
*
|
|
1299
|
+
* @param {boolean} inline_styles True → emit style="…"; false → class="…"
|
|
1300
|
+
* @param {Object} styles The QUIKDOWN_STYLES map
|
|
1301
|
+
* @returns {Function}
|
|
1302
|
+
*/
|
|
1188
1303
|
function createGetAttr(inline_styles, styles) {
|
|
1189
1304
|
return function(tag, additionalStyle = '') {
|
|
1190
1305
|
if (inline_styles) {
|
|
1191
1306
|
let style = styles[tag];
|
|
1192
1307
|
if (!style && !additionalStyle) return '';
|
|
1193
|
-
|
|
1194
|
-
//
|
|
1308
|
+
|
|
1309
|
+
// When adding alignment that conflicts with the tag's default,
|
|
1310
|
+
// strip the default text-align first.
|
|
1195
1311
|
if (additionalStyle && additionalStyle.includes('text-align') && style && style.includes('text-align')) {
|
|
1196
1312
|
style = style.replace(/text-align:[^;]+;?/, '').trim();
|
|
1313
|
+
/* istanbul ignore next */
|
|
1197
1314
|
if (style && !style.endsWith(';')) style += ';';
|
|
1198
1315
|
}
|
|
1199
|
-
|
|
1316
|
+
|
|
1200
1317
|
/* istanbul ignore next - defensive: additionalStyle without style doesn't occur with current tags */
|
|
1201
1318
|
const fullStyle = additionalStyle ? (style ? `${style}${additionalStyle}` : additionalStyle) : style;
|
|
1202
1319
|
return ` style="${fullStyle}"`;
|
|
1203
1320
|
} else {
|
|
1204
1321
|
const classAttr = ` class="${CLASS_PREFIX}${tag}"`;
|
|
1205
|
-
// Apply inline styles for alignment even when using CSS classes
|
|
1206
1322
|
if (additionalStyle) {
|
|
1207
1323
|
return `${classAttr} style="${additionalStyle}"`;
|
|
1208
1324
|
}
|
|
@@ -1211,69 +1327,84 @@
|
|
|
1211
1327
|
};
|
|
1212
1328
|
}
|
|
1213
1329
|
|
|
1330
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1331
|
+
// Main parser function
|
|
1332
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1333
|
+
|
|
1214
1334
|
function quikdown(markdown, options = {}) {
|
|
1335
|
+
// ── Guard: only process non-empty strings ──
|
|
1215
1336
|
if (!markdown || typeof markdown !== 'string') {
|
|
1216
1337
|
return '';
|
|
1217
1338
|
}
|
|
1218
|
-
|
|
1219
|
-
const { fence_plugin, inline_styles = false, bidirectional = false, lazy_linefeeds = false } = options;
|
|
1220
|
-
const styles = QUIKDOWN_STYLES; // Use module-level styles
|
|
1221
|
-
const getAttr = createGetAttr(inline_styles, styles); // Create getAttr once
|
|
1222
1339
|
|
|
1223
|
-
//
|
|
1340
|
+
// ── Unpack options ──
|
|
1341
|
+
const { fence_plugin, inline_styles = false, bidirectional = false, lazy_linefeeds = false, allow_unsafe_html = false } = options;
|
|
1342
|
+
const styles = QUIKDOWN_STYLES;
|
|
1343
|
+
const getAttr = createGetAttr(inline_styles, styles);
|
|
1344
|
+
|
|
1345
|
+
// ── Helpers (closed over options) ──
|
|
1346
|
+
|
|
1347
|
+
/** Escape the five HTML-special characters. */
|
|
1224
1348
|
function escapeHtml(text) {
|
|
1225
1349
|
return text.replace(/[&<>"']/g, m => ESC_MAP[m]);
|
|
1226
1350
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* Bidirectional marker helper.
|
|
1354
|
+
* When bidirectional mode is on, returns ` data-qd="…"`.
|
|
1355
|
+
* The non-bidirectional branch is a trivial no-op arrow; it is
|
|
1356
|
+
* exercised in the core bundle but never in quikdown_bd.
|
|
1357
|
+
*/
|
|
1358
|
+
/* istanbul ignore next - trivial no-op fallback */
|
|
1229
1359
|
const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
|
|
1230
|
-
|
|
1231
|
-
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Sanitize a URL to block javascript:, vbscript:, and non-image data: URIs.
|
|
1363
|
+
* Returns '#' for blocked URLs.
|
|
1364
|
+
*/
|
|
1232
1365
|
function sanitizeUrl(url, allowUnsafe = false) {
|
|
1233
1366
|
/* istanbul ignore next - defensive programming, regex ensures url is never empty */
|
|
1234
1367
|
if (!url) return '';
|
|
1235
|
-
|
|
1236
|
-
// If unsafe URLs are explicitly allowed, return as-is
|
|
1237
1368
|
if (allowUnsafe) return url;
|
|
1238
|
-
|
|
1369
|
+
|
|
1239
1370
|
const trimmedUrl = url.trim();
|
|
1240
1371
|
const lowerUrl = trimmedUrl.toLowerCase();
|
|
1241
|
-
|
|
1242
|
-
// Block dangerous protocols
|
|
1243
1372
|
const dangerousProtocols = ['javascript:', 'vbscript:', 'data:'];
|
|
1244
|
-
|
|
1373
|
+
|
|
1245
1374
|
for (const protocol of dangerousProtocols) {
|
|
1246
1375
|
if (lowerUrl.startsWith(protocol)) {
|
|
1247
|
-
// Exception: Allow data:image/* for images
|
|
1248
1376
|
if (protocol === 'data:' && lowerUrl.startsWith('data:image/')) {
|
|
1249
1377
|
return trimmedUrl;
|
|
1250
1378
|
}
|
|
1251
|
-
// Return safe empty link for dangerous protocols
|
|
1252
1379
|
return '#';
|
|
1253
1380
|
}
|
|
1254
1381
|
}
|
|
1255
|
-
|
|
1256
1382
|
return trimmedUrl;
|
|
1257
1383
|
}
|
|
1258
1384
|
|
|
1259
|
-
//
|
|
1385
|
+
// ────────────────────────────────────────────────────────────────
|
|
1386
|
+
// Phase 1 — Code Extraction
|
|
1387
|
+
// ────────────────────────────────────────────────────────────────
|
|
1388
|
+
// Why extract code first? Fenced blocks and inline code spans can
|
|
1389
|
+
// contain markdown-like characters (*, _, #, |, etc.) that must NOT
|
|
1390
|
+
// be interpreted as formatting. By pulling them out and replacing
|
|
1391
|
+
// with unique placeholders, the rest of the pipeline never sees them.
|
|
1392
|
+
|
|
1260
1393
|
let html = markdown;
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
// Fence must be at start of line
|
|
1394
|
+
const codeBlocks = []; // Array of {lang, code, custom, fence, hasReverse}
|
|
1395
|
+
const inlineCodes = []; // Array of escaped-HTML strings
|
|
1396
|
+
|
|
1397
|
+
// ── Fenced code blocks ──
|
|
1398
|
+
// Matches paired fences: ``` with ``` and ~~~ with ~~~.
|
|
1399
|
+
// The fence must start at column 0 of a line (^ with /m flag).
|
|
1400
|
+
// Group 1 = fence marker, Group 2 = language hint, Group 3 = code body.
|
|
1269
1401
|
html = html.replace(/^(```|~~~)([^\n]*)\n([\s\S]*?)^\1$/gm, (match, fence, lang, code) => {
|
|
1270
1402
|
const placeholder = `${PLACEHOLDER_CB}${codeBlocks.length}§`;
|
|
1271
|
-
|
|
1272
|
-
// Trim the language specification
|
|
1273
1403
|
const langTrimmed = lang ? lang.trim() : '';
|
|
1274
|
-
|
|
1275
|
-
// If custom fence plugin is provided, use it (v1.1.0: object format required)
|
|
1404
|
+
|
|
1276
1405
|
if (fence_plugin && fence_plugin.render && typeof fence_plugin.render === 'function') {
|
|
1406
|
+
// Custom plugin — store raw code (un-escaped) so the plugin
|
|
1407
|
+
// receives the original source.
|
|
1277
1408
|
codeBlocks.push({
|
|
1278
1409
|
lang: langTrimmed,
|
|
1279
1410
|
code: code.trimEnd(),
|
|
@@ -1282,6 +1413,7 @@
|
|
|
1282
1413
|
hasReverse: !!fence_plugin.reverse
|
|
1283
1414
|
});
|
|
1284
1415
|
} else {
|
|
1416
|
+
// Default — pre-escape the code for safe HTML output.
|
|
1285
1417
|
codeBlocks.push({
|
|
1286
1418
|
lang: langTrimmed,
|
|
1287
1419
|
code: escapeHtml(code.trimEnd()),
|
|
@@ -1291,66 +1423,97 @@
|
|
|
1291
1423
|
}
|
|
1292
1424
|
return placeholder;
|
|
1293
1425
|
});
|
|
1294
|
-
|
|
1295
|
-
//
|
|
1426
|
+
|
|
1427
|
+
// ── Inline code spans ──
|
|
1428
|
+
// Matches a single backtick pair: `content`.
|
|
1429
|
+
// Content is captured and HTML-escaped immediately.
|
|
1296
1430
|
html = html.replace(/`([^`]+)`/g, (match, code) => {
|
|
1297
1431
|
const placeholder = `${PLACEHOLDER_IC}${inlineCodes.length}§`;
|
|
1298
1432
|
inlineCodes.push(escapeHtml(code));
|
|
1299
1433
|
return placeholder;
|
|
1300
1434
|
});
|
|
1301
|
-
|
|
1302
|
-
//
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
//
|
|
1306
|
-
|
|
1307
|
-
//
|
|
1435
|
+
|
|
1436
|
+
// ────────────────────────────────────────────────────────────────
|
|
1437
|
+
// Phase 2 — HTML Escaping
|
|
1438
|
+
// ────────────────────────────────────────────────────────────────
|
|
1439
|
+
// All remaining text (everything except code placeholders) is escaped
|
|
1440
|
+
// to prevent XSS. The `allow_unsafe_html` option skips this for
|
|
1441
|
+
// trusted pipelines that intentionally embed raw HTML.
|
|
1442
|
+
|
|
1443
|
+
if (!allow_unsafe_html) {
|
|
1444
|
+
html = escapeHtml(html);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// ────────────────────────────────────────────────────────────────
|
|
1448
|
+
// Phase 3 — Block Scanning + Inline Formatting + Paragraphs
|
|
1449
|
+
// ────────────────────────────────────────────────────────────────
|
|
1450
|
+
// This is the heart of the lexer rewrite. Instead of applying
|
|
1451
|
+
// 10+ global regex passes, we:
|
|
1452
|
+
// 1. Process tables (line walker — tables need multi-line lookahead)
|
|
1453
|
+
// 2. Scan remaining lines for headings, HR, blockquotes
|
|
1454
|
+
// 3. Process lists (line walker — lists need indent tracking)
|
|
1455
|
+
// 4. Apply inline formatting to all text content
|
|
1456
|
+
// 5. Wrap remaining text in <p> tags
|
|
1457
|
+
//
|
|
1458
|
+
// Steps 1 and 3 are line-walkers that process the full text in a
|
|
1459
|
+
// single pass each. Step 2 replaces global regex with a per-line
|
|
1460
|
+
// scanner. Steps 4-5 are applied to the result.
|
|
1461
|
+
//
|
|
1462
|
+
// Total: 3 structured passes instead of 10+ regex passes.
|
|
1463
|
+
|
|
1464
|
+
// ── Step 1: Tables ──
|
|
1465
|
+
// Tables need multi-line lookahead (header → separator → body rows)
|
|
1466
|
+
// so they're handled by a dedicated line-walker first.
|
|
1308
1467
|
html = processTable(html, getAttr);
|
|
1309
|
-
|
|
1310
|
-
//
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
//
|
|
1317
|
-
|
|
1318
|
-
// Merge consecutive blockquotes
|
|
1319
|
-
html = html.replace(/<\/blockquote>\n<blockquote>/g, '\n');
|
|
1320
|
-
|
|
1321
|
-
// Process horizontal rules (allow trailing spaces)
|
|
1322
|
-
html = html.replace(/^---+\s*$/gm, `<hr${getAttr('hr')}>`);
|
|
1323
|
-
|
|
1324
|
-
// Process lists
|
|
1468
|
+
|
|
1469
|
+
// ── Step 2: Headings, HR, Blockquotes ──
|
|
1470
|
+
// These are simple line-level constructs. We scan each line once
|
|
1471
|
+
// and replace matching lines with their HTML representation.
|
|
1472
|
+
html = scanLineBlocks(html, getAttr, dataQd);
|
|
1473
|
+
|
|
1474
|
+
// ── Step 3: Lists ──
|
|
1475
|
+
// Lists need indent-level tracking across lines, so they get their
|
|
1476
|
+
// own line-walker.
|
|
1325
1477
|
html = processLists(html, getAttr, inline_styles, bidirectional);
|
|
1326
|
-
|
|
1327
|
-
//
|
|
1328
|
-
|
|
1329
|
-
//
|
|
1478
|
+
|
|
1479
|
+
// ── Step 4: Inline formatting ──
|
|
1480
|
+
// Apply bold, italic, strikethrough, images, links, and autolinks
|
|
1481
|
+
// to all text content. This runs on the output of steps 1-3, so
|
|
1482
|
+
// it sees text inside headings, blockquotes, table cells, list
|
|
1483
|
+
// items, and paragraph text.
|
|
1484
|
+
|
|
1485
|
+
// Images (must come before links —  vs [text](url))
|
|
1330
1486
|
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
|
1331
1487
|
const sanitizedSrc = sanitizeUrl(src, options.allow_unsafe_urls);
|
|
1488
|
+
/* istanbul ignore next - bd-only branch */
|
|
1332
1489
|
const altAttr = bidirectional && alt ? ` data-qd-alt="${escapeHtml(alt)}"` : '';
|
|
1490
|
+
/* istanbul ignore next - bd-only branch */
|
|
1333
1491
|
const srcAttr = bidirectional ? ` data-qd-src="${escapeHtml(src)}"` : '';
|
|
1334
1492
|
return `<img${getAttr('img')} src="${sanitizedSrc}" alt="${alt}"${altAttr}${srcAttr}${dataQd('!')}>`;
|
|
1335
1493
|
});
|
|
1336
|
-
|
|
1337
|
-
// Links
|
|
1494
|
+
|
|
1495
|
+
// Links
|
|
1338
1496
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
|
|
1339
|
-
// Sanitize URL to prevent XSS
|
|
1340
1497
|
const sanitizedHref = sanitizeUrl(href, options.allow_unsafe_urls);
|
|
1341
1498
|
const isExternal = /^https?:\/\//i.test(sanitizedHref);
|
|
1342
1499
|
const rel = isExternal ? ' rel="noopener noreferrer"' : '';
|
|
1500
|
+
/* istanbul ignore next - bd-only branch */
|
|
1343
1501
|
const textAttr = bidirectional ? ` data-qd-text="${escapeHtml(text)}"` : '';
|
|
1344
1502
|
return `<a${getAttr('a')} href="${sanitizedHref}"${rel}${textAttr}${dataQd('[')}>${text}</a>`;
|
|
1345
1503
|
});
|
|
1346
|
-
|
|
1347
|
-
// Autolinks
|
|
1504
|
+
|
|
1505
|
+
// Autolinks — bare https?:// URLs become clickable <a> tags
|
|
1348
1506
|
html = html.replace(/(^|\s)(https?:\/\/[^\s<]+)/g, (match, prefix, url) => {
|
|
1349
1507
|
const sanitizedUrl = sanitizeUrl(url, options.allow_unsafe_urls);
|
|
1350
1508
|
return `${prefix}<a${getAttr('a')} href="${sanitizedUrl}" rel="noopener noreferrer">${url}</a>`;
|
|
1351
1509
|
});
|
|
1352
|
-
|
|
1353
|
-
//
|
|
1510
|
+
|
|
1511
|
+
// Protect rendered tags so emphasis regexes don't see attribute
|
|
1512
|
+
// values — fixes #3 (underscores in URLs interpreted as emphasis).
|
|
1513
|
+
const savedTags = [];
|
|
1514
|
+
html = html.replace(/<[^>]+>/g, m => { savedTags.push(m); return `%%T${savedTags.length - 1}%%`; });
|
|
1515
|
+
|
|
1516
|
+
// Bold, italic, strikethrough
|
|
1354
1517
|
const inlinePatterns = [
|
|
1355
1518
|
[/\*\*(.+?)\*\*/g, 'strong', '**'],
|
|
1356
1519
|
[/__(.+?)__/g, 'strong', '__'],
|
|
@@ -1358,60 +1521,66 @@
|
|
|
1358
1521
|
[/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, 'em', '_'],
|
|
1359
1522
|
[/~~(.+?)~~/g, 'del', '~~']
|
|
1360
1523
|
];
|
|
1361
|
-
|
|
1362
1524
|
inlinePatterns.forEach(([pattern, tag, marker]) => {
|
|
1363
1525
|
html = html.replace(pattern, `<${tag}${getAttr(tag)}${dataQd(marker)}>$1</${tag}>`);
|
|
1364
1526
|
});
|
|
1365
|
-
|
|
1366
|
-
//
|
|
1527
|
+
|
|
1528
|
+
// Restore protected tags
|
|
1529
|
+
html = html.replace(/%%T(\d+)%%/g, (_, i) => savedTags[i]);
|
|
1530
|
+
|
|
1531
|
+
// ── Step 5: Line breaks + paragraph wrapping ──
|
|
1367
1532
|
if (lazy_linefeeds) {
|
|
1368
|
-
// Lazy linefeeds: single
|
|
1533
|
+
// Lazy linefeeds mode: every single \n becomes <br> EXCEPT:
|
|
1534
|
+
// • Double newlines → paragraph break
|
|
1535
|
+
// • Newlines adjacent to block elements (h, blockquote, pre, hr, table, list)
|
|
1536
|
+
//
|
|
1537
|
+
// Strategy: protect block-adjacent newlines with §N§, convert
|
|
1538
|
+
// the rest, then restore.
|
|
1539
|
+
|
|
1369
1540
|
const blocks = [];
|
|
1370
1541
|
let bi = 0;
|
|
1371
|
-
|
|
1372
|
-
// Protect tables and lists
|
|
1542
|
+
|
|
1543
|
+
// Protect tables and lists from <br> injection
|
|
1373
1544
|
html = html.replace(/<(table|[uo]l)[^>]*>[\s\S]*?<\/\1>/g, m => {
|
|
1374
1545
|
blocks[bi] = m;
|
|
1375
1546
|
return `§B${bi++}§`;
|
|
1376
1547
|
});
|
|
1377
|
-
|
|
1378
|
-
// Handle paragraphs and block elements
|
|
1548
|
+
|
|
1379
1549
|
html = html.replace(/\n\n+/g, '§P§')
|
|
1380
|
-
// After block
|
|
1550
|
+
// After block-level closing tags
|
|
1381
1551
|
.replace(/(<\/(?:h[1-6]|blockquote|pre)>)\n/g, '$1§N§')
|
|
1382
1552
|
.replace(/(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)\n/g, '$1§N§')
|
|
1383
|
-
// Before block
|
|
1553
|
+
// Before block-level opening tags
|
|
1384
1554
|
.replace(/\n(<(?:h[1-6]|blockquote|pre|hr)[^>]*>)/g, '§N§$1')
|
|
1385
1555
|
.replace(/\n(§B\d+§)/g, '§N§$1')
|
|
1386
1556
|
.replace(/(§B\d+§)\n/g, '$1§N§')
|
|
1387
|
-
// Convert
|
|
1557
|
+
// Convert surviving newlines to <br>
|
|
1388
1558
|
.replace(/\n/g, `<br${getAttr('br')}>`)
|
|
1389
1559
|
// Restore
|
|
1390
1560
|
.replace(/§N§/g, '\n')
|
|
1391
1561
|
.replace(/§P§/g, '</p><p>');
|
|
1392
|
-
|
|
1562
|
+
|
|
1393
1563
|
// Restore protected blocks
|
|
1394
1564
|
blocks.forEach((b, i) => html = html.replace(`§B${i}§`, b));
|
|
1395
|
-
|
|
1565
|
+
|
|
1396
1566
|
html = '<p>' + html + '</p>';
|
|
1397
1567
|
} else {
|
|
1398
|
-
// Standard: two spaces
|
|
1399
|
-
html = html.replace(/
|
|
1400
|
-
|
|
1401
|
-
// Paragraphs (double newlines)
|
|
1402
|
-
// Don't add </p> after block elements (they're not in paragraphs)
|
|
1568
|
+
// Standard mode: two trailing spaces → <br>, double newline → new paragraph
|
|
1569
|
+
html = html.replace(/ {2}$/gm, `<br${getAttr('br')}>`);
|
|
1570
|
+
|
|
1403
1571
|
html = html.replace(/\n\n+/g, (match, offset) => {
|
|
1404
|
-
// Check if we're after a block element closing tag
|
|
1405
1572
|
const before = html.substring(0, offset);
|
|
1406
1573
|
if (before.match(/<\/(h[1-6]|blockquote|ul|ol|table|pre|hr)>$/)) {
|
|
1407
|
-
return '<p>';
|
|
1574
|
+
return '<p>';
|
|
1408
1575
|
}
|
|
1409
|
-
return '</p><p>';
|
|
1576
|
+
return '</p><p>';
|
|
1410
1577
|
});
|
|
1411
1578
|
html = '<p>' + html + '</p>';
|
|
1412
1579
|
}
|
|
1413
|
-
|
|
1414
|
-
//
|
|
1580
|
+
|
|
1581
|
+
// ── Step 6: Cleanup ──
|
|
1582
|
+
// Remove <p> wrappers that accidentally enclose block elements.
|
|
1583
|
+
// This is simpler than trying to prevent them during wrapping.
|
|
1415
1584
|
const cleanupPatterns = [
|
|
1416
1585
|
[/<p><\/p>/g, ''],
|
|
1417
1586
|
[/<p>(<h[1-6][^>]*>)/g, '$1'],
|
|
@@ -1425,67 +1594,154 @@
|
|
|
1425
1594
|
[/(<\/table>)<\/p>/g, '$1'],
|
|
1426
1595
|
[/<p>(<pre[^>]*>)/g, '$1'],
|
|
1427
1596
|
[/(<\/pre>)<\/p>/g, '$1'],
|
|
1428
|
-
[new RegExp(`<p>(${PLACEHOLDER_CB}\\d+§)
|
|
1597
|
+
[new RegExp(`<p>(${PLACEHOLDER_CB}\\d+§)</p>`, 'g'), '$1']
|
|
1429
1598
|
];
|
|
1430
|
-
|
|
1431
1599
|
cleanupPatterns.forEach(([pattern, replacement]) => {
|
|
1432
1600
|
html = html.replace(pattern, replacement);
|
|
1433
1601
|
});
|
|
1434
|
-
|
|
1435
|
-
//
|
|
1436
|
-
// When a paragraph follows a block element, ensure it has opening <p>
|
|
1602
|
+
|
|
1603
|
+
// When a block element is followed by a newline and then text, open a <p>.
|
|
1437
1604
|
html = html.replace(/(<\/(?:h[1-6]|blockquote|ul|ol|table|pre|hr)>)\n([^<])/g, '$1\n<p>$2');
|
|
1438
|
-
|
|
1439
|
-
//
|
|
1440
|
-
|
|
1441
|
-
//
|
|
1605
|
+
|
|
1606
|
+
// ────────────────────────────────────────────────────────────────
|
|
1607
|
+
// Phase 4 — Code Restoration
|
|
1608
|
+
// ────────────────────────────────────────────────────────────────
|
|
1609
|
+
// Replace placeholders with rendered HTML. For fenced blocks this
|
|
1610
|
+
// means wrapping in <pre><code>…</code></pre> (or calling the
|
|
1611
|
+
// fence_plugin). For inline code it means <code>…</code>.
|
|
1612
|
+
|
|
1442
1613
|
codeBlocks.forEach((block, i) => {
|
|
1443
1614
|
let replacement;
|
|
1444
|
-
|
|
1615
|
+
|
|
1445
1616
|
if (block.custom && fence_plugin && fence_plugin.render) {
|
|
1446
|
-
//
|
|
1617
|
+
// Delegate to the user-provided fence plugin.
|
|
1447
1618
|
replacement = fence_plugin.render(block.code, block.lang);
|
|
1448
|
-
|
|
1449
|
-
// If plugin returns undefined, fall back to default rendering
|
|
1619
|
+
|
|
1450
1620
|
if (replacement === undefined) {
|
|
1621
|
+
// Plugin declined — fall back to default rendering.
|
|
1451
1622
|
const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
|
|
1452
1623
|
const codeAttr = inline_styles ? getAttr('code') : langClass;
|
|
1624
|
+
/* istanbul ignore next - bd-only branch */
|
|
1453
1625
|
const langAttr = bidirectional && block.lang ? ` data-qd-lang="${escapeHtml(block.lang)}"` : '';
|
|
1626
|
+
/* istanbul ignore next - bd-only branch */
|
|
1454
1627
|
const fenceAttr = bidirectional ? ` data-qd-fence="${escapeHtml(block.fence)}"` : '';
|
|
1455
1628
|
replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${escapeHtml(block.code)}</code></pre>`;
|
|
1456
|
-
} else if (bidirectional) {
|
|
1457
|
-
//
|
|
1458
|
-
replacement = replacement.replace(/^<(\w+)/,
|
|
1629
|
+
} else /* istanbul ignore next - bd-only branch */ if (bidirectional) {
|
|
1630
|
+
// Plugin returned HTML — inject data attributes for roundtrip.
|
|
1631
|
+
replacement = replacement.replace(/^<(\w+)/,
|
|
1459
1632
|
`<$1 data-qd-fence="${escapeHtml(block.fence)}" data-qd-lang="${escapeHtml(block.lang)}" data-qd-source="${escapeHtml(block.code)}"`);
|
|
1460
1633
|
}
|
|
1461
1634
|
} else {
|
|
1462
|
-
// Default rendering
|
|
1635
|
+
// Default rendering — wrap in <pre><code>.
|
|
1463
1636
|
const langClass = !inline_styles && block.lang ? ` class="language-${block.lang}"` : '';
|
|
1464
1637
|
const codeAttr = inline_styles ? getAttr('code') : langClass;
|
|
1638
|
+
/* istanbul ignore next - bd-only branch */
|
|
1465
1639
|
const langAttr = bidirectional && block.lang ? ` data-qd-lang="${escapeHtml(block.lang)}"` : '';
|
|
1640
|
+
/* istanbul ignore next - bd-only branch */
|
|
1466
1641
|
const fenceAttr = bidirectional ? ` data-qd-fence="${escapeHtml(block.fence)}"` : '';
|
|
1467
1642
|
replacement = `<pre${getAttr('pre')}${fenceAttr}${langAttr}><code${codeAttr}>${block.code}</code></pre>`;
|
|
1468
1643
|
}
|
|
1469
|
-
|
|
1644
|
+
|
|
1470
1645
|
const placeholder = `${PLACEHOLDER_CB}${i}§`;
|
|
1471
1646
|
html = html.replace(placeholder, replacement);
|
|
1472
1647
|
});
|
|
1473
|
-
|
|
1474
|
-
// Restore inline code
|
|
1648
|
+
|
|
1649
|
+
// Restore inline code spans
|
|
1475
1650
|
inlineCodes.forEach((code, i) => {
|
|
1476
1651
|
const placeholder = `${PLACEHOLDER_IC}${i}§`;
|
|
1477
1652
|
html = html.replace(placeholder, `<code${getAttr('code')}${dataQd('`')}>${code}</code>`);
|
|
1478
1653
|
});
|
|
1479
|
-
|
|
1654
|
+
|
|
1480
1655
|
return html.trim();
|
|
1481
1656
|
}
|
|
1482
1657
|
|
|
1658
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1659
|
+
// Block-level line scanner
|
|
1660
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* scanLineBlocks — single-pass line scanner for headings, HR, blockquotes
|
|
1664
|
+
*
|
|
1665
|
+
* Walks the text line by line. For each line it checks (in order):
|
|
1666
|
+
* 1. Heading — starts with 1-6 '#' followed by a space
|
|
1667
|
+
* 2. HR — line is entirely '---…' (3+ dashes, optional trailing space)
|
|
1668
|
+
* 3. Blockquote — starts with '> ' (the > was already HTML-escaped)
|
|
1669
|
+
*
|
|
1670
|
+
* Lines that don't match any block pattern are passed through unchanged.
|
|
1671
|
+
*
|
|
1672
|
+
* This replaces three separate global regex passes from the pre-1.2.8
|
|
1673
|
+
* architecture with one structured scan.
|
|
1674
|
+
*
|
|
1675
|
+
* @param {string} text The document text (HTML-escaped, code extracted)
|
|
1676
|
+
* @param {Function} getAttr Attribute factory (class or style)
|
|
1677
|
+
* @param {Function} dataQd Bidirectional marker factory
|
|
1678
|
+
* @returns {string} Text with block-level elements rendered
|
|
1679
|
+
*/
|
|
1680
|
+
function scanLineBlocks(text, getAttr, dataQd) {
|
|
1681
|
+
const lines = text.split('\n');
|
|
1682
|
+
const result = [];
|
|
1683
|
+
let i = 0;
|
|
1684
|
+
|
|
1685
|
+
while (i < lines.length) {
|
|
1686
|
+
const line = lines[i];
|
|
1687
|
+
|
|
1688
|
+
// ── Heading ──
|
|
1689
|
+
// Count leading '#' characters. Valid heading: 1-6 hashes then a space.
|
|
1690
|
+
// Example: "## Hello World ##" → <h2>Hello World</h2>
|
|
1691
|
+
let hashCount = 0;
|
|
1692
|
+
while (hashCount < line.length && hashCount < 7 && line[hashCount] === '#') {
|
|
1693
|
+
hashCount++;
|
|
1694
|
+
}
|
|
1695
|
+
if (hashCount >= 1 && hashCount <= 6 && line[hashCount] === ' ') {
|
|
1696
|
+
// Extract content after "# " and strip trailing hashes
|
|
1697
|
+
const content = line.slice(hashCount + 1).replace(/\s*#+\s*$/, '');
|
|
1698
|
+
const tag = 'h' + hashCount;
|
|
1699
|
+
result.push(`<${tag}${getAttr(tag)}${dataQd('#'.repeat(hashCount))}>${content}</${tag}>`);
|
|
1700
|
+
i++;
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// ── Horizontal Rule ──
|
|
1705
|
+
// Three or more dashes, optional trailing whitespace, nothing else.
|
|
1706
|
+
if (isDashHRLine(line)) {
|
|
1707
|
+
result.push(`<hr${getAttr('hr')}>`);
|
|
1708
|
+
i++;
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// ── Blockquote ──
|
|
1713
|
+
// After Phase 2, the '>' character has been escaped to '>'.
|
|
1714
|
+
// Pattern: "> content" or merged consecutive blockquotes.
|
|
1715
|
+
if (/^>\s+/.test(line)) {
|
|
1716
|
+
result.push(`<blockquote${getAttr('blockquote')}>${line.replace(/^>\s+/, '')}</blockquote>`);
|
|
1717
|
+
i++;
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// ── Pass-through ──
|
|
1722
|
+
result.push(line);
|
|
1723
|
+
i++;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// Merge consecutive blockquotes into a single element.
|
|
1727
|
+
// <blockquote>A</blockquote>\n<blockquote>B</blockquote>
|
|
1728
|
+
// → <blockquote>A\nB</blockquote>
|
|
1729
|
+
let joined = result.join('\n');
|
|
1730
|
+
joined = joined.replace(/<\/blockquote>\n<blockquote>/g, '\n');
|
|
1731
|
+
return joined;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1735
|
+
// Table processing (line walker)
|
|
1736
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1737
|
+
|
|
1483
1738
|
/**
|
|
1484
|
-
*
|
|
1739
|
+
* Inline markdown formatter for table cells.
|
|
1740
|
+
* Handles bold, italic, strikethrough, and code within cell text.
|
|
1741
|
+
* Links / images / autolinks are handled by the global inline pass
|
|
1742
|
+
* (Phase 3 Step 4) which runs after table processing.
|
|
1485
1743
|
*/
|
|
1486
1744
|
function processInlineMarkdown(text, getAttr) {
|
|
1487
|
-
|
|
1488
|
-
// Process inline formatting patterns
|
|
1489
1745
|
const patterns = [
|
|
1490
1746
|
[/\*\*(.+?)\*\*/g, 'strong'],
|
|
1491
1747
|
[/__(.+?)__/g, 'strong'],
|
|
@@ -1494,27 +1750,32 @@
|
|
|
1494
1750
|
[/~~(.+?)~~/g, 'del'],
|
|
1495
1751
|
[/`([^`]+)`/g, 'code']
|
|
1496
1752
|
];
|
|
1497
|
-
|
|
1498
1753
|
patterns.forEach(([pattern, tag]) => {
|
|
1499
1754
|
text = text.replace(pattern, `<${tag}${getAttr(tag)}>$1</${tag}>`);
|
|
1500
1755
|
});
|
|
1501
|
-
|
|
1502
1756
|
return text;
|
|
1503
1757
|
}
|
|
1504
1758
|
|
|
1505
1759
|
/**
|
|
1506
|
-
*
|
|
1760
|
+
* processTable — line walker for markdown tables
|
|
1761
|
+
*
|
|
1762
|
+
* Walks through lines looking for runs of pipe-containing lines.
|
|
1763
|
+
* Each run is validated (must contain a separator row: |---|---|)
|
|
1764
|
+
* and rendered as an HTML <table>. Invalid runs are restored as-is.
|
|
1765
|
+
*
|
|
1766
|
+
* @param {string} text Full document text
|
|
1767
|
+
* @param {Function} getAttr Attribute factory
|
|
1768
|
+
* @returns {string} Text with tables rendered
|
|
1507
1769
|
*/
|
|
1508
1770
|
function processTable(text, getAttr) {
|
|
1509
1771
|
const lines = text.split('\n');
|
|
1510
1772
|
const result = [];
|
|
1511
1773
|
let inTable = false;
|
|
1512
1774
|
let tableLines = [];
|
|
1513
|
-
|
|
1775
|
+
|
|
1514
1776
|
for (let i = 0; i < lines.length; i++) {
|
|
1515
1777
|
const line = lines[i].trim();
|
|
1516
|
-
|
|
1517
|
-
// Check if this line looks like a table row (with or without trailing |)
|
|
1778
|
+
|
|
1518
1779
|
if (line.includes('|') && (line.startsWith('|') || /[^\\|]/.test(line))) {
|
|
1519
1780
|
if (!inTable) {
|
|
1520
1781
|
inTable = true;
|
|
@@ -1522,14 +1783,11 @@
|
|
|
1522
1783
|
}
|
|
1523
1784
|
tableLines.push(line);
|
|
1524
1785
|
} else {
|
|
1525
|
-
// Not a table line
|
|
1526
1786
|
if (inTable) {
|
|
1527
|
-
// Process the accumulated table
|
|
1528
1787
|
const tableHtml = buildTable(tableLines, getAttr);
|
|
1529
1788
|
if (tableHtml) {
|
|
1530
1789
|
result.push(tableHtml);
|
|
1531
1790
|
} else {
|
|
1532
|
-
// Not a valid table, restore original lines
|
|
1533
1791
|
result.push(...tableLines);
|
|
1534
1792
|
}
|
|
1535
1793
|
inTable = false;
|
|
@@ -1538,8 +1796,8 @@
|
|
|
1538
1796
|
result.push(lines[i]);
|
|
1539
1797
|
}
|
|
1540
1798
|
}
|
|
1541
|
-
|
|
1542
|
-
// Handle table at end of
|
|
1799
|
+
|
|
1800
|
+
// Handle table at end of document
|
|
1543
1801
|
if (inTable && tableLines.length > 0) {
|
|
1544
1802
|
const tableHtml = buildTable(tableLines, getAttr);
|
|
1545
1803
|
if (tableHtml) {
|
|
@@ -1548,35 +1806,35 @@
|
|
|
1548
1806
|
result.push(...tableLines);
|
|
1549
1807
|
}
|
|
1550
1808
|
}
|
|
1551
|
-
|
|
1809
|
+
|
|
1552
1810
|
return result.join('\n');
|
|
1553
1811
|
}
|
|
1554
1812
|
|
|
1555
1813
|
/**
|
|
1556
|
-
*
|
|
1814
|
+
* buildTable — validate and render a table from accumulated lines
|
|
1815
|
+
*
|
|
1816
|
+
* @param {string[]} lines Array of pipe-containing lines
|
|
1817
|
+
* @param {Function} getAttr Attribute factory
|
|
1818
|
+
* @returns {string|null} HTML table string, or null if invalid
|
|
1557
1819
|
*/
|
|
1558
1820
|
function buildTable(lines, getAttr) {
|
|
1559
|
-
|
|
1560
1821
|
if (lines.length < 2) return null;
|
|
1561
|
-
|
|
1562
|
-
//
|
|
1822
|
+
|
|
1823
|
+
// Find the separator row (---|---|)
|
|
1563
1824
|
let separatorIndex = -1;
|
|
1564
1825
|
for (let i = 1; i < lines.length; i++) {
|
|
1565
|
-
// Support separator with or without leading/trailing pipes
|
|
1566
1826
|
if (/^\|?[\s\-:|]+\|?$/.test(lines[i]) && lines[i].includes('-')) {
|
|
1567
1827
|
separatorIndex = i;
|
|
1568
1828
|
break;
|
|
1569
1829
|
}
|
|
1570
1830
|
}
|
|
1571
|
-
|
|
1572
1831
|
if (separatorIndex === -1) return null;
|
|
1573
|
-
|
|
1832
|
+
|
|
1574
1833
|
const headerLines = lines.slice(0, separatorIndex);
|
|
1575
1834
|
const bodyLines = lines.slice(separatorIndex + 1);
|
|
1576
|
-
|
|
1577
|
-
// Parse alignment from separator
|
|
1835
|
+
|
|
1836
|
+
// Parse alignment from separator cells (:--- = left, :---: = center, ---: = right)
|
|
1578
1837
|
const separator = lines[separatorIndex];
|
|
1579
|
-
// Handle pipes at start/end or not
|
|
1580
1838
|
const separatorCells = separator.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
|
|
1581
1839
|
const alignments = separatorCells.map(cell => {
|
|
1582
1840
|
const trimmed = cell.trim();
|
|
@@ -1584,31 +1842,28 @@
|
|
|
1584
1842
|
if (trimmed.endsWith(':')) return 'right';
|
|
1585
1843
|
return 'left';
|
|
1586
1844
|
});
|
|
1587
|
-
|
|
1845
|
+
|
|
1588
1846
|
let html = `<table${getAttr('table')}>\n`;
|
|
1589
|
-
|
|
1590
|
-
//
|
|
1591
|
-
// Note: headerLines will always have length > 0 since separatorIndex starts from 1
|
|
1847
|
+
|
|
1848
|
+
// Header
|
|
1592
1849
|
html += `<thead${getAttr('thead')}>\n`;
|
|
1593
1850
|
headerLines.forEach(line => {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
html += '</tr>\n';
|
|
1851
|
+
html += `<tr${getAttr('tr')}>\n`;
|
|
1852
|
+
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
|
|
1853
|
+
cells.forEach((cell, i) => {
|
|
1854
|
+
const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
|
|
1855
|
+
const processedCell = processInlineMarkdown(cell.trim(), getAttr);
|
|
1856
|
+
html += `<th${getAttr('th', alignStyle)}>${processedCell}</th>\n`;
|
|
1857
|
+
});
|
|
1858
|
+
html += '</tr>\n';
|
|
1603
1859
|
});
|
|
1604
1860
|
html += '</thead>\n';
|
|
1605
|
-
|
|
1606
|
-
//
|
|
1861
|
+
|
|
1862
|
+
// Body
|
|
1607
1863
|
if (bodyLines.length > 0) {
|
|
1608
1864
|
html += `<tbody${getAttr('tbody')}>\n`;
|
|
1609
1865
|
bodyLines.forEach(line => {
|
|
1610
1866
|
html += `<tr${getAttr('tr')}>\n`;
|
|
1611
|
-
// Handle pipes at start/end or not
|
|
1612
1867
|
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|');
|
|
1613
1868
|
cells.forEach((cell, i) => {
|
|
1614
1869
|
const alignStyle = alignments[i] && alignments[i] !== 'left' ? `text-align:${alignments[i]}` : '';
|
|
@@ -1619,61 +1874,81 @@
|
|
|
1619
1874
|
});
|
|
1620
1875
|
html += '</tbody>\n';
|
|
1621
1876
|
}
|
|
1622
|
-
|
|
1877
|
+
|
|
1623
1878
|
html += '</table>';
|
|
1624
1879
|
return html;
|
|
1625
1880
|
}
|
|
1626
1881
|
|
|
1882
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1883
|
+
// List processing (line walker)
|
|
1884
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1885
|
+
|
|
1627
1886
|
/**
|
|
1628
|
-
*
|
|
1887
|
+
* processLists — line walker for ordered, unordered, and task lists
|
|
1888
|
+
*
|
|
1889
|
+
* Scans each line for list markers (-, *, +, 1., 2., etc.) with
|
|
1890
|
+
* optional leading indentation for nesting. Non-list lines close
|
|
1891
|
+
* any open lists and pass through unchanged.
|
|
1892
|
+
*
|
|
1893
|
+
* Task lists (- [ ] / - [x]) are detected and rendered with
|
|
1894
|
+
* checkbox inputs.
|
|
1895
|
+
*
|
|
1896
|
+
* @param {string} text Full document text
|
|
1897
|
+
* @param {Function} getAttr Attribute factory
|
|
1898
|
+
* @param {boolean} inline_styles Whether to use inline styles
|
|
1899
|
+
* @param {boolean} bidirectional Whether to add data-qd markers
|
|
1900
|
+
* @returns {string} Text with lists rendered
|
|
1629
1901
|
*/
|
|
1630
1902
|
function processLists(text, getAttr, inline_styles, bidirectional) {
|
|
1631
|
-
|
|
1632
1903
|
const lines = text.split('\n');
|
|
1633
1904
|
const result = [];
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
// Helper to escape HTML for data-qd attributes
|
|
1637
|
-
|
|
1905
|
+
const listStack = []; // tracks nesting: [{type:'ul', level:0}, …]
|
|
1906
|
+
|
|
1907
|
+
// Helper to escape HTML for data-qd attributes. List markers (`-`, `*`,
|
|
1908
|
+
// `+`, `1.`, etc.) never contain HTML-special chars, so the replace
|
|
1909
|
+
// callback is defensive-only and never actually fires in practice.
|
|
1910
|
+
/* istanbul ignore next - defensive: list markers never trigger escaping */
|
|
1911
|
+
const escapeHtml = (text) => text.replace(/[&<>"']/g,
|
|
1912
|
+
/* istanbul ignore next - defensive: list markers never contain HTML specials */
|
|
1913
|
+
m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]);
|
|
1914
|
+
/* istanbul ignore next - trivial no-op fallback; not exercised via bd bundle */
|
|
1638
1915
|
const dataQd = bidirectional ? (marker) => ` data-qd="${escapeHtml(marker)}"` : () => '';
|
|
1639
|
-
|
|
1916
|
+
|
|
1640
1917
|
for (let i = 0; i < lines.length; i++) {
|
|
1641
1918
|
const line = lines[i];
|
|
1642
1919
|
const match = line.match(/^(\s*)([*\-+]|\d+\.)\s+(.+)$/);
|
|
1643
|
-
|
|
1920
|
+
|
|
1644
1921
|
if (match) {
|
|
1645
1922
|
const [, indent, marker, content] = match;
|
|
1646
1923
|
const level = Math.floor(indent.length / 2);
|
|
1647
1924
|
const isOrdered = /^\d+\./.test(marker);
|
|
1648
1925
|
const listType = isOrdered ? 'ol' : 'ul';
|
|
1649
|
-
|
|
1650
|
-
//
|
|
1926
|
+
|
|
1927
|
+
// Task list detection (only in unordered lists)
|
|
1651
1928
|
let listItemContent = content;
|
|
1652
1929
|
let taskListClass = '';
|
|
1653
1930
|
const taskMatch = content.match(/^\[([x ])\]\s+(.*)$/i);
|
|
1654
1931
|
if (taskMatch && !isOrdered) {
|
|
1655
1932
|
const [, checked, taskContent] = taskMatch;
|
|
1656
1933
|
const isChecked = checked.toLowerCase() === 'x';
|
|
1657
|
-
const checkboxAttr = inline_styles
|
|
1658
|
-
? ' style="margin-right:.5em"'
|
|
1934
|
+
const checkboxAttr = inline_styles
|
|
1935
|
+
? ' style="margin-right:.5em"'
|
|
1659
1936
|
: ` class="${CLASS_PREFIX}task-checkbox"`;
|
|
1660
1937
|
listItemContent = `<input type="checkbox"${checkboxAttr}${isChecked ? ' checked' : ''} disabled> ${taskContent}`;
|
|
1661
1938
|
taskListClass = inline_styles ? ' style="list-style:none"' : ` class="${CLASS_PREFIX}task-item"`;
|
|
1662
1939
|
}
|
|
1663
|
-
|
|
1664
|
-
// Close deeper levels
|
|
1940
|
+
|
|
1941
|
+
// Close deeper nesting levels
|
|
1665
1942
|
while (listStack.length > level + 1) {
|
|
1666
1943
|
const list = listStack.pop();
|
|
1667
1944
|
result.push(`</${list.type}>`);
|
|
1668
1945
|
}
|
|
1669
|
-
|
|
1670
|
-
// Open new
|
|
1946
|
+
|
|
1947
|
+
// Open new list or switch type at current level
|
|
1671
1948
|
if (listStack.length === level) {
|
|
1672
|
-
// Need to open a new list
|
|
1673
1949
|
listStack.push({ type: listType, level });
|
|
1674
1950
|
result.push(`<${listType}${getAttr(listType)}>`);
|
|
1675
1951
|
} else if (listStack.length === level + 1) {
|
|
1676
|
-
// Check if we need to switch list type
|
|
1677
1952
|
const currentList = listStack[listStack.length - 1];
|
|
1678
1953
|
if (currentList.type !== listType) {
|
|
1679
1954
|
result.push(`</${currentList.type}>`);
|
|
@@ -1682,11 +1957,11 @@
|
|
|
1682
1957
|
result.push(`<${listType}${getAttr(listType)}>`);
|
|
1683
1958
|
}
|
|
1684
1959
|
}
|
|
1685
|
-
|
|
1960
|
+
|
|
1686
1961
|
const liAttr = taskListClass || getAttr('li');
|
|
1687
1962
|
result.push(`<li${liAttr}${dataQd(marker)}>${listItemContent}</li>`);
|
|
1688
1963
|
} else {
|
|
1689
|
-
// Not a list item
|
|
1964
|
+
// Not a list item — close all open lists
|
|
1690
1965
|
while (listStack.length > 0) {
|
|
1691
1966
|
const list = listStack.pop();
|
|
1692
1967
|
result.push(`</${list.type}>`);
|
|
@@ -1694,76 +1969,76 @@
|
|
|
1694
1969
|
result.push(line);
|
|
1695
1970
|
}
|
|
1696
1971
|
}
|
|
1697
|
-
|
|
1698
|
-
// Close any remaining lists
|
|
1972
|
+
|
|
1973
|
+
// Close any remaining open lists
|
|
1699
1974
|
while (listStack.length > 0) {
|
|
1700
1975
|
const list = listStack.pop();
|
|
1701
1976
|
result.push(`</${list.type}>`);
|
|
1702
1977
|
}
|
|
1703
|
-
|
|
1978
|
+
|
|
1704
1979
|
return result.join('\n');
|
|
1705
1980
|
}
|
|
1706
1981
|
|
|
1982
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1983
|
+
// Static API
|
|
1984
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1985
|
+
|
|
1707
1986
|
/**
|
|
1708
|
-
* Emit CSS
|
|
1709
|
-
*
|
|
1710
|
-
* @param {string}
|
|
1711
|
-
* @
|
|
1987
|
+
* Emit CSS rules for all quikdown elements.
|
|
1988
|
+
*
|
|
1989
|
+
* @param {string} prefix Class prefix (default: 'quikdown-')
|
|
1990
|
+
* @param {string} theme 'light' (default) or 'dark'
|
|
1991
|
+
* @returns {string} CSS text
|
|
1712
1992
|
*/
|
|
1713
1993
|
quikdown.emitStyles = function(prefix = 'quikdown-', theme = 'light') {
|
|
1714
1994
|
const styles = QUIKDOWN_STYLES;
|
|
1715
|
-
|
|
1716
|
-
// Define theme color overrides
|
|
1995
|
+
|
|
1717
1996
|
const themeOverrides = {
|
|
1718
1997
|
dark: {
|
|
1719
|
-
'#f4f4f4': '#2a2a2a',
|
|
1720
|
-
'#f0f0f0': '#2a2a2a',
|
|
1721
|
-
'#f2f2f2': '#2a2a2a',
|
|
1722
|
-
'#ddd': '#3a3a3a',
|
|
1723
|
-
'#06c': '#6db3f2',
|
|
1998
|
+
'#f4f4f4': '#2a2a2a', // pre background
|
|
1999
|
+
'#f0f0f0': '#2a2a2a', // code background
|
|
2000
|
+
'#f2f2f2': '#2a2a2a', // th background
|
|
2001
|
+
'#ddd': '#3a3a3a', // borders
|
|
2002
|
+
'#06c': '#6db3f2', // links
|
|
1724
2003
|
_textColor: '#e0e0e0'
|
|
1725
2004
|
},
|
|
1726
2005
|
light: {
|
|
1727
|
-
_textColor: '#333'
|
|
2006
|
+
_textColor: '#333'
|
|
1728
2007
|
}
|
|
1729
2008
|
};
|
|
1730
|
-
|
|
2009
|
+
|
|
1731
2010
|
let css = '';
|
|
1732
2011
|
for (const [tag, style] of Object.entries(styles)) {
|
|
1733
2012
|
let themedStyle = style;
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
if (!oldColor.startsWith('_')) {
|
|
1740
|
-
themedStyle = themedStyle.replace(new RegExp(oldColor, 'g'), newColor);
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
// Add text color for certain elements in dark theme
|
|
1745
|
-
const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
|
|
1746
|
-
if (needsTextColor.includes(tag)) {
|
|
1747
|
-
themedStyle += `;color:${themeOverrides.dark._textColor}`;
|
|
1748
|
-
}
|
|
1749
|
-
} else if (theme === 'light' && themeOverrides.light) {
|
|
1750
|
-
// Add explicit text color for light theme elements too
|
|
1751
|
-
const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
|
|
1752
|
-
if (needsTextColor.includes(tag)) {
|
|
1753
|
-
themedStyle += `;color:${themeOverrides.light._textColor}`;
|
|
2013
|
+
|
|
2014
|
+
if (theme === 'dark' && themeOverrides.dark) {
|
|
2015
|
+
for (const [oldColor, newColor] of Object.entries(themeOverrides.dark)) {
|
|
2016
|
+
if (!oldColor.startsWith('_')) {
|
|
2017
|
+
themedStyle = themedStyle.replaceAll(oldColor, newColor);
|
|
1754
2018
|
}
|
|
1755
2019
|
}
|
|
1756
|
-
|
|
2020
|
+
const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
|
|
2021
|
+
if (needsTextColor.includes(tag)) {
|
|
2022
|
+
themedStyle += `;color:${themeOverrides.dark._textColor}`;
|
|
2023
|
+
}
|
|
2024
|
+
} else if (theme === 'light' && themeOverrides.light) {
|
|
2025
|
+
const needsTextColor = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'li', 'blockquote'];
|
|
2026
|
+
if (needsTextColor.includes(tag)) {
|
|
2027
|
+
themedStyle += `;color:${themeOverrides.light._textColor}`;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
1757
2031
|
css += `.${prefix}${tag} { ${themedStyle} }\n`;
|
|
1758
2032
|
}
|
|
1759
|
-
|
|
2033
|
+
|
|
1760
2034
|
return css;
|
|
1761
2035
|
};
|
|
1762
2036
|
|
|
1763
2037
|
/**
|
|
1764
|
-
*
|
|
1765
|
-
*
|
|
1766
|
-
* @
|
|
2038
|
+
* Create a pre-configured parser with baked-in options.
|
|
2039
|
+
*
|
|
2040
|
+
* @param {Object} options Options to bake in
|
|
2041
|
+
* @returns {Function} Configured quikdown(markdown) function
|
|
1767
2042
|
*/
|
|
1768
2043
|
quikdown.configure = function(options) {
|
|
1769
2044
|
return function(markdown) {
|
|
@@ -1771,18 +2046,18 @@
|
|
|
1771
2046
|
};
|
|
1772
2047
|
};
|
|
1773
2048
|
|
|
1774
|
-
/**
|
|
1775
|
-
* Version information
|
|
1776
|
-
*/
|
|
2049
|
+
/** Semantic version (injected at build time) */
|
|
1777
2050
|
quikdown.version = quikdownVersion;
|
|
1778
2051
|
|
|
1779
|
-
//
|
|
2052
|
+
// ════════════════════════════════════════════════════════════════════
|
|
2053
|
+
// Exports
|
|
2054
|
+
// ════════════════════════════════════════════════════════════════════
|
|
2055
|
+
|
|
1780
2056
|
/* istanbul ignore next */
|
|
1781
2057
|
if (typeof module !== 'undefined' && module.exports) {
|
|
1782
2058
|
module.exports = quikdown;
|
|
1783
2059
|
}
|
|
1784
2060
|
|
|
1785
|
-
// For browser global
|
|
1786
2061
|
/* istanbul ignore next */
|
|
1787
2062
|
if (typeof window !== 'undefined') {
|
|
1788
2063
|
window.quikdown = quikdown;
|
|
@@ -1806,8 +2081,11 @@
|
|
|
1806
2081
|
return quikdown(markdown, { ...options, bidirectional: true });
|
|
1807
2082
|
}
|
|
1808
2083
|
|
|
1809
|
-
// Copy all properties and methods from quikdown (including version)
|
|
2084
|
+
// Copy all properties and methods from quikdown (including version).
|
|
2085
|
+
// Skip `configure` — quikdown_bd provides its own override below, so the
|
|
2086
|
+
// inner quikdown.configure is dead code in this bundle.
|
|
1810
2087
|
Object.keys(quikdown).forEach(key => {
|
|
2088
|
+
if (key === 'configure') return;
|
|
1811
2089
|
quikdown_bd[key] = quikdown[key];
|
|
1812
2090
|
});
|
|
1813
2091
|
|
|
@@ -1841,7 +2119,7 @@
|
|
|
1841
2119
|
|
|
1842
2120
|
// Process children with context
|
|
1843
2121
|
let childContent = '';
|
|
1844
|
-
for (
|
|
2122
|
+
for (const child of node.childNodes) {
|
|
1845
2123
|
childContent += walkNode(child, { parentTag: tag, ...parentContext });
|
|
1846
2124
|
}
|
|
1847
2125
|
|
|
@@ -2075,7 +2353,7 @@
|
|
|
2075
2353
|
let index = 1;
|
|
2076
2354
|
const indent = ' '.repeat(depth);
|
|
2077
2355
|
|
|
2078
|
-
for (
|
|
2356
|
+
for (const child of listNode.children) {
|
|
2079
2357
|
if (child.tagName !== 'LI') continue;
|
|
2080
2358
|
|
|
2081
2359
|
const dataQd = child.getAttribute('data-qd');
|
|
@@ -2088,7 +2366,7 @@
|
|
|
2088
2366
|
marker = '-';
|
|
2089
2367
|
// Get text without the checkbox
|
|
2090
2368
|
let text = '';
|
|
2091
|
-
for (
|
|
2369
|
+
for (const node of child.childNodes) {
|
|
2092
2370
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
2093
2371
|
text += node.textContent;
|
|
2094
2372
|
} else if (node.tagName && node.tagName !== 'INPUT') {
|
|
@@ -2099,7 +2377,7 @@
|
|
|
2099
2377
|
} else {
|
|
2100
2378
|
let itemContent = '';
|
|
2101
2379
|
|
|
2102
|
-
for (
|
|
2380
|
+
for (const node of child.childNodes) {
|
|
2103
2381
|
if (node.tagName === 'UL' || node.tagName === 'OL') {
|
|
2104
2382
|
itemContent += walkList(node, node.tagName === 'OL', depth + 1);
|
|
2105
2383
|
} else {
|
|
@@ -2128,7 +2406,7 @@
|
|
|
2128
2406
|
const headerRow = thead.querySelector('tr');
|
|
2129
2407
|
if (headerRow) {
|
|
2130
2408
|
const headers = [];
|
|
2131
|
-
for (
|
|
2409
|
+
for (const th of headerRow.querySelectorAll('th')) {
|
|
2132
2410
|
headers.push(th.textContent.trim());
|
|
2133
2411
|
}
|
|
2134
2412
|
result += '| ' + headers.join(' | ') + ' |\n';
|
|
@@ -2147,9 +2425,9 @@
|
|
|
2147
2425
|
// Process body
|
|
2148
2426
|
const tbody = table.querySelector('tbody');
|
|
2149
2427
|
if (tbody) {
|
|
2150
|
-
for (
|
|
2428
|
+
for (const row of tbody.querySelectorAll('tr')) {
|
|
2151
2429
|
const cells = [];
|
|
2152
|
-
for (
|
|
2430
|
+
for (const td of row.querySelectorAll('td')) {
|
|
2153
2431
|
cells.push(td.textContent.trim());
|
|
2154
2432
|
}
|
|
2155
2433
|
if (cells.length > 0) {
|
|
@@ -2171,10 +2449,13 @@
|
|
|
2171
2449
|
return markdown;
|
|
2172
2450
|
};
|
|
2173
2451
|
|
|
2174
|
-
// Override the configure method to return a bidirectional version
|
|
2452
|
+
// Override the configure method to return a bidirectional version.
|
|
2453
|
+
// We delegate to the inner quikdown.configure so the shared closure
|
|
2454
|
+
// machinery is exercised in both bundles (no dead code).
|
|
2175
2455
|
quikdown_bd.configure = function(options) {
|
|
2456
|
+
const innerParser = quikdown.configure({ ...options, bidirectional: true });
|
|
2176
2457
|
return function(markdown) {
|
|
2177
|
-
return
|
|
2458
|
+
return innerParser(markdown);
|
|
2178
2459
|
};
|
|
2179
2460
|
};
|
|
2180
2461
|
|