webforai 4.2.0 → 4.3.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.
Files changed (33) hide show
  1. package/dist/adapters/sites/youtube.js +4 -2
  2. package/dist/bin.js +662 -474
  3. package/dist/cjs/adapters/sites/youtube.js +5 -2
  4. package/dist/cjs/detect-client-shell.js +2 -0
  5. package/dist/cjs/extractors/lib/terminators.js +2 -0
  6. package/dist/cjs/index.js +3 -0
  7. package/dist/cjs/mdast-handlers/custom-code-handler.js +31 -2
  8. package/dist/cjs/normalize/index.js +97 -4
  9. package/dist/cjs/utils/detect-code-lang.js +4 -5
  10. package/dist/cjs/utils/parse-html.js +73 -5
  11. package/dist/detect-client-shell.js +1 -0
  12. package/dist/extractors/lib/terminators.js +2 -0
  13. package/dist/index.js +2 -0
  14. package/dist/mdast-handlers/custom-code-handler.js +31 -2
  15. package/dist/normalize/index.js +94 -3
  16. package/dist/types/adapters/sites/youtube.d.ts +2 -0
  17. package/dist/types/adapters/sites/youtube.js.map +1 -1
  18. package/dist/types/detect-client-shell.d.ts +2 -0
  19. package/dist/types/detect-client-shell.js.map +1 -1
  20. package/dist/types/extractors/lib/terminators.js.map +1 -1
  21. package/dist/types/index.d.ts +1 -0
  22. package/dist/types/index.js.map +1 -1
  23. package/dist/types/mdast-handlers/custom-code-handler.d.ts +5 -0
  24. package/dist/types/mdast-handlers/custom-code-handler.js.map +1 -1
  25. package/dist/types/normalize/index.d.ts +21 -0
  26. package/dist/types/normalize/index.js.map +1 -1
  27. package/dist/types/utils/detect-code-lang.d.ts +7 -1
  28. package/dist/types/utils/detect-code-lang.js.map +1 -1
  29. package/dist/types/utils/parse-html.d.ts +12 -6
  30. package/dist/types/utils/parse-html.js.map +1 -1
  31. package/dist/utils/detect-code-lang.js +4 -5
  32. package/dist/utils/parse-html.js +73 -5
  33. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -6,7 +6,7 @@ import { CommanderError, program } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "webforai",
9
- version: "4.2.0",
9
+ version: "4.3.0",
10
10
  description: "Convert web pages and local HTML to clean, LLM-ready Markdown: library, CLI (npx webforai) and hosted-API client.",
11
11
  author: "inaridiy",
12
12
  license: "Apache-2.0",
@@ -1360,8 +1360,440 @@ import path5 from "node:path";
1360
1360
  // src/utils/parse-html.ts
1361
1361
  import { fromParse5 } from "hast-util-from-parse5";
1362
1362
  import { parse, parseFragment } from "parse5";
1363
+
1364
+ // src/utils/hast-fast.ts
1365
+ var EMPTY_METRICS = { text: 0, link: 0, commas: 0, paragraphs: 0, elements: 0 };
1366
+ var COMMA_REGEX = /[,、,،]/g;
1367
+ var PARAGRAPH_LIKE = /* @__PURE__ */ new Set(["p", "pre", "li", "blockquote", "figcaption", "dd", "dt"]);
1368
+ var NON_PROSE_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "svg", "math", "iframe", "object"]);
1369
+ var isElement = (node) => typeof node === "object" && node !== null && node.type === "element";
1370
+ var isParent = (node) => typeof node === "object" && node !== null && Array.isArray(node.children);
1371
+ var classList = (element4) => {
1372
+ const className = element4.properties?.className;
1373
+ if (Array.isArray(className)) {
1374
+ return className;
1375
+ }
1376
+ if (typeof className === "string") {
1377
+ return className.split(/\s+/).filter(Boolean);
1378
+ }
1379
+ return [];
1380
+ };
1381
+ var matchString = (element4) => {
1382
+ const id = element4.properties?.id;
1383
+ const classes = classList(element4);
1384
+ if (!id && classes.length === 0) {
1385
+ return element4.tagName;
1386
+ }
1387
+ return `${element4.tagName} ${typeof id === "string" ? id : ""} ${classes.join(" ")}`;
1388
+ };
1389
+ var MetricsCollector = class {
1390
+ #cache = /* @__PURE__ */ new WeakMap();
1391
+ metrics(node) {
1392
+ if (node.type === "text") {
1393
+ const value = node.value;
1394
+ if (value.trim().length === 0) {
1395
+ return EMPTY_METRICS;
1396
+ }
1397
+ return {
1398
+ text: value.length,
1399
+ link: 0,
1400
+ commas: (value.match(COMMA_REGEX) ?? []).length,
1401
+ paragraphs: 0,
1402
+ elements: 0
1403
+ };
1404
+ }
1405
+ if (!isParent(node)) {
1406
+ return EMPTY_METRICS;
1407
+ }
1408
+ const cached = this.#cache.get(node);
1409
+ if (cached) {
1410
+ return cached;
1411
+ }
1412
+ if (isElement(node) && NON_PROSE_TAGS.has(node.tagName)) {
1413
+ this.#cache.set(node, EMPTY_METRICS);
1414
+ return EMPTY_METRICS;
1415
+ }
1416
+ let text7 = 0;
1417
+ let link = 0;
1418
+ let commas = 0;
1419
+ let paragraphs = 0;
1420
+ let elements = 0;
1421
+ for (const child of node.children) {
1422
+ const childMetrics = this.metrics(child);
1423
+ text7 += childMetrics.text;
1424
+ link += childMetrics.link;
1425
+ commas += childMetrics.commas;
1426
+ paragraphs += childMetrics.paragraphs;
1427
+ elements += childMetrics.elements;
1428
+ if (isElement(child)) {
1429
+ elements += 1;
1430
+ if (PARAGRAPH_LIKE.has(child.tagName)) {
1431
+ paragraphs += 1;
1432
+ }
1433
+ }
1434
+ }
1435
+ if (isElement(node) && node.tagName === "a") {
1436
+ link = text7;
1437
+ }
1438
+ const metrics = { text: text7, link, commas, paragraphs, elements };
1439
+ this.#cache.set(node, metrics);
1440
+ return metrics;
1441
+ }
1442
+ textLength(node) {
1443
+ return this.metrics(node).text;
1444
+ }
1445
+ /** Fraction of a subtree's text that is link anchor text. `0` for empty subtrees. */
1446
+ linkDensity(node) {
1447
+ const { text: text7, link } = this.metrics(node);
1448
+ if (text7 === 0) {
1449
+ return 0;
1450
+ }
1451
+ return Math.min(1, link / text7);
1452
+ }
1453
+ };
1454
+ var pruneInPlace = (tree, keep) => {
1455
+ if (!isParent(tree)) {
1456
+ return tree;
1457
+ }
1458
+ const stack = [tree];
1459
+ while (stack.length > 0) {
1460
+ const parent = stack.pop();
1461
+ const children = parent.children;
1462
+ let write = 0;
1463
+ for (let read = 0; read < children.length; read++) {
1464
+ const child = children[read];
1465
+ if (!keep(child, parent)) {
1466
+ continue;
1467
+ }
1468
+ children[write++] = child;
1469
+ if (isParent(child)) {
1470
+ stack.push(child);
1471
+ }
1472
+ }
1473
+ children.length = write;
1474
+ }
1475
+ return tree;
1476
+ };
1477
+ var walk = (tree, visit) => {
1478
+ const stack = [{ node: tree, parent: void 0 }];
1479
+ while (stack.length > 0) {
1480
+ const { node, parent } = stack.pop();
1481
+ if (visit(node, parent) === false) {
1482
+ continue;
1483
+ }
1484
+ if (isParent(node)) {
1485
+ for (let i = node.children.length - 1; i >= 0; i--) {
1486
+ stack.push({ node: node.children[i], parent: node });
1487
+ }
1488
+ }
1489
+ }
1490
+ };
1491
+ var collectElements = (tree, predicate) => {
1492
+ const found = [];
1493
+ walk(tree, (node) => {
1494
+ if (isElement(node) && predicate(node)) {
1495
+ found.push(node);
1496
+ }
1497
+ });
1498
+ return found;
1499
+ };
1500
+ var findElement = (tree, predicate) => {
1501
+ let found;
1502
+ walk(tree, (node) => {
1503
+ if (found) {
1504
+ return false;
1505
+ }
1506
+ if (isElement(node) && predicate(node)) {
1507
+ found = node;
1508
+ return false;
1509
+ }
1510
+ });
1511
+ return found;
1512
+ };
1513
+ var cloneHast = (tree) => {
1514
+ try {
1515
+ return structuredClone(tree);
1516
+ } catch {
1517
+ return manualClone(tree);
1518
+ }
1519
+ };
1520
+ var manualClone = (node) => {
1521
+ if (Array.isArray(node)) {
1522
+ return node.map(manualClone);
1523
+ }
1524
+ if (typeof node !== "object" || node === null) {
1525
+ return node;
1526
+ }
1527
+ const copy = {};
1528
+ for (const [key, value] of Object.entries(node)) {
1529
+ copy[key] = manualClone(value);
1530
+ }
1531
+ return copy;
1532
+ };
1533
+ var property = (element4, key) => {
1534
+ const properties = element4.properties;
1535
+ if (!properties) {
1536
+ return void 0;
1537
+ }
1538
+ const direct = properties[key];
1539
+ if (direct !== void 0) {
1540
+ return direct;
1541
+ }
1542
+ if (!(key.includes("-") || key.includes(":"))) {
1543
+ return void 0;
1544
+ }
1545
+ return properties[camelCaseAttribute(key)];
1546
+ };
1547
+ var camelCaseAttribute = (key) => key.replace(/[-:]([a-z])/g, (_, character) => character.toUpperCase());
1548
+ var numericProperty = (element4, key) => {
1549
+ const raw = property(element4, key);
1550
+ if (typeof raw === "number") {
1551
+ return Number.isFinite(raw) ? raw : void 0;
1552
+ }
1553
+ if (typeof raw === "string" && raw.length > 0) {
1554
+ const parsed = Number.parseFloat(raw);
1555
+ return Number.isFinite(parsed) ? parsed : void 0;
1556
+ }
1557
+ return void 0;
1558
+ };
1559
+ var stringProperty = (element4, key) => {
1560
+ const raw = property(element4, key);
1561
+ if (typeof raw === "string") {
1562
+ return raw;
1563
+ }
1564
+ if (Array.isArray(raw)) {
1565
+ return raw.join(" ");
1566
+ }
1567
+ if (raw === true) {
1568
+ return "";
1569
+ }
1570
+ return void 0;
1571
+ };
1572
+ var isTruthyAttribute = (element4, key) => {
1573
+ const raw = property(element4, key);
1574
+ if (raw === void 0 || raw === null || raw === false) {
1575
+ return false;
1576
+ }
1577
+ if (raw === true) {
1578
+ return true;
1579
+ }
1580
+ if (typeof raw === "string") {
1581
+ return raw.toLowerCase() !== "false";
1582
+ }
1583
+ return true;
1584
+ };
1585
+
1586
+ // src/adapters/sites/youtube.ts
1587
+ var findObjectEnd = (source, start) => {
1588
+ let depth = 0;
1589
+ let inString = false;
1590
+ let escaped = false;
1591
+ for (let index = start; index < source.length; index++) {
1592
+ const character = source[index];
1593
+ if (escaped) {
1594
+ escaped = false;
1595
+ } else if (character === "\\") {
1596
+ escaped = true;
1597
+ } else if (character === '"') {
1598
+ inString = !inString;
1599
+ } else if (!inString) {
1600
+ if (character === "{") {
1601
+ depth += 1;
1602
+ } else if (character === "}" && --depth === 0) {
1603
+ return index + 1;
1604
+ }
1605
+ }
1606
+ }
1607
+ return -1;
1608
+ };
1609
+ var extractAssignedJson = (source, name) => {
1610
+ const markerIndex = source.indexOf(`${name} =`);
1611
+ if (markerIndex === -1) {
1612
+ return void 0;
1613
+ }
1614
+ const start = source.indexOf("{", markerIndex);
1615
+ if (start === -1) {
1616
+ return void 0;
1617
+ }
1618
+ const end = findObjectEnd(source, start);
1619
+ if (end === -1) {
1620
+ return void 0;
1621
+ }
1622
+ try {
1623
+ return JSON.parse(source.slice(start, end));
1624
+ } catch {
1625
+ return void 0;
1626
+ }
1627
+ };
1628
+ var scriptText = (root) => collectElements(root, (element4) => element4.tagName === "script").map(
1629
+ (script) => script.children.map((child) => child.type === "text" ? child.value : "").join("")
1630
+ );
1631
+ var PLAYER_RESPONSE_VARIABLE = "ytInitialPlayerResponse";
1632
+ var findPlayerResponse = (root) => {
1633
+ for (const source of scriptText(root)) {
1634
+ if (!source.includes(PLAYER_RESPONSE_VARIABLE)) {
1635
+ continue;
1636
+ }
1637
+ const parsed = extractAssignedJson(source, PLAYER_RESPONSE_VARIABLE);
1638
+ if (parsed && typeof parsed === "object") {
1639
+ return parsed;
1640
+ }
1641
+ }
1642
+ return void 0;
1643
+ };
1644
+ var youtubeCaptionTracks = (root) => {
1645
+ const player = findPlayerResponse(root);
1646
+ const renderer = player?.captions?.playerCaptionsTracklistRenderer;
1647
+ const tracks = renderer?.captionTracks;
1648
+ if (!Array.isArray(tracks)) {
1649
+ return [];
1650
+ }
1651
+ return tracks.flatMap((track) => {
1652
+ const url = typeof track.baseUrl === "string" ? track.baseUrl : void 0;
1653
+ const languageCode = typeof track.languageCode === "string" ? track.languageCode : void 0;
1654
+ if (!(url && languageCode)) {
1655
+ return [];
1656
+ }
1657
+ const name = track.name?.simpleText;
1658
+ return [
1659
+ {
1660
+ url,
1661
+ languageCode,
1662
+ name: typeof name === "string" ? name : void 0,
1663
+ isAutomatic: track.kind === "asr"
1664
+ }
1665
+ ];
1666
+ });
1667
+ };
1668
+ var text3 = (value) => ({ type: "text", value });
1669
+ var element = (tagName, children) => ({
1670
+ type: "element",
1671
+ tagName,
1672
+ properties: {},
1673
+ children
1674
+ });
1675
+ var heading = (depth, value) => element(`h${depth}`, [text3(value)]);
1676
+ var paragraph = (value) => element("p", [text3(value)]);
1677
+ var descriptionNodes = (description) => description.split(/\n{2,}/).map((block) => block.trim()).filter((block) => block.length > 0).map(
1678
+ (block) => element(
1679
+ "p",
1680
+ block.split("\n").flatMap((line, index) => index === 0 ? [text3(line)] : [element("br", []), text3(line)])
1681
+ )
1682
+ );
1683
+ var formatDuration = (seconds) => {
1684
+ const hours = Math.floor(seconds / 3600);
1685
+ const minutes = Math.floor(seconds % 3600 / 60);
1686
+ const remainder = seconds % 60;
1687
+ const pad = (value) => String(value).padStart(2, "0");
1688
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(remainder)}` : `${minutes}:${pad(remainder)}`;
1689
+ };
1690
+ var metadataList = (details) => {
1691
+ const items = [];
1692
+ if (details.author) {
1693
+ items.push(element("li", [text3(`Channel: ${details.author}`)]));
1694
+ }
1695
+ const seconds = Number(details.lengthSeconds);
1696
+ if (Number.isFinite(seconds) && seconds > 0) {
1697
+ items.push(element("li", [text3(`Duration: ${formatDuration(seconds)}`)]));
1698
+ }
1699
+ const views = Number(details.viewCount);
1700
+ if (Number.isFinite(views) && views > 0) {
1701
+ items.push(element("li", [text3(`Views: ${views.toLocaleString("en-US")}`)]));
1702
+ }
1703
+ return items.length > 0 ? element("ul", items) : void 0;
1704
+ };
1705
+ var youtubeAdapter = {
1706
+ id: "youtube",
1707
+ matches: ({ url }) => {
1708
+ if (!url) {
1709
+ return false;
1710
+ }
1711
+ try {
1712
+ return /(^|\.)(youtube\.com|youtu\.be|youtube-nocookie\.com)$/.test(new URL(url).hostname);
1713
+ } catch {
1714
+ return false;
1715
+ }
1716
+ },
1717
+ // Scanning every script body for the JSON blob would cost a full traversal on every page, so
1718
+ // the fallback keys off markers the signature already collected.
1719
+ matchesFingerprint: ({ signature }) => Boolean(signature?.ids.has("movie_player") || signature?.classes.has("ytd-app")),
1720
+ extract: ({ hast }) => {
1721
+ const player = findPlayerResponse(hast);
1722
+ const details = player?.videoDetails;
1723
+ if (!details?.title) {
1724
+ return void 0;
1725
+ }
1726
+ const children = [heading(1, details.title)];
1727
+ const metadata = metadataList(details);
1728
+ if (metadata) {
1729
+ children.push(metadata);
1730
+ }
1731
+ const description = details.shortDescription?.trim();
1732
+ if (description) {
1733
+ children.push(heading(2, "Description"));
1734
+ children.push(...descriptionNodes(description));
1735
+ }
1736
+ const tracks = youtubeCaptionTracks(hast);
1737
+ if (tracks.length > 0) {
1738
+ const languages2 = [...new Set(tracks.map((track) => track.languageCode))].sort();
1739
+ children.push(paragraph(`Captions available: ${languages2.join(", ")}`));
1740
+ }
1741
+ return { type: "root", children };
1742
+ }
1743
+ };
1744
+
1745
+ // src/detect-client-shell.ts
1746
+ var SHELL_SUSPICION_THRESHOLD = 300;
1747
+ var SHELL_EMPTY_THRESHOLD = 50;
1748
+ var BODY_PATTERN = /<body[^>]*>([\s\S]*?)<\/body>/i;
1749
+ var HEAD_PATTERN = /<head[^>]*>[\s\S]*?<\/head>/gi;
1750
+ var NOSCRIPT_PATTERN = /<noscript[^>]*>([\s\S]*?)<\/noscript>/gi;
1751
+ var INVISIBLE_ELEMENT_PATTERN = /<(script|style|template|svg|noscript)[^>]*>[\s\S]*?<\/\1>/gi;
1752
+ var COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
1753
+ var TAG_PATTERN = /<[^>]+>/g;
1754
+ var MOUNT_POINT_PATTERN = /<[a-z][^>]*\sid=["'](?:root|app|__next|___gatsby|__nuxt|q-app|svelte|react-root)["']|<app-root[\s>]|\sdata-reactroot[\s>=]/i;
1755
+ var CHALLENGE_PATTERN = /cf-chl|challenge-platform|cf_chl_opt|just a moment|attention required|checking your browser|_incapsula_|px-captcha|datadome/i;
1756
+ var ENTITIES = {
1757
+ "&nbsp;": " ",
1758
+ "&amp;": "&",
1759
+ "&lt;": "<",
1760
+ "&gt;": ">",
1761
+ "&quot;": '"',
1762
+ "&#39;": "'"
1763
+ };
1764
+ var visibleTextOf = (markup) => markup.replace(COMMENT_PATTERN, " ").replace(INVISIBLE_ELEMENT_PATTERN, " ").replace(TAG_PATTERN, " ").replace(/&(?:nbsp|amp|lt|gt|quot|#39);/g, (entity) => ENTITIES[entity] ?? " ").replace(/\s+/g, " ").trim();
1765
+ var noscriptMentionsJavascript = (html) => {
1766
+ for (const match of html.matchAll(NOSCRIPT_PATTERN)) {
1767
+ if (/javascript/i.test(match[1] ?? "")) {
1768
+ return true;
1769
+ }
1770
+ }
1771
+ return false;
1772
+ };
1773
+ var detectClientShell = (html) => {
1774
+ const body = BODY_PATTERN.exec(html)?.[1] ?? html.replace(HEAD_PATTERN, " ");
1775
+ const visibleTextLength = visibleTextOf(body).length;
1776
+ if (visibleTextLength >= SHELL_SUSPICION_THRESHOLD) {
1777
+ return { isShell: false, visibleTextLength };
1778
+ }
1779
+ if (CHALLENGE_PATTERN.test(html)) {
1780
+ return { isShell: true, reason: "anti-bot-challenge", visibleTextLength };
1781
+ }
1782
+ if (MOUNT_POINT_PATTERN.test(body)) {
1783
+ return { isShell: true, reason: "spa-shell", visibleTextLength };
1784
+ }
1785
+ if (noscriptMentionsJavascript(html)) {
1786
+ return { isShell: true, reason: "noscript-only", visibleTextLength };
1787
+ }
1788
+ if (visibleTextLength < SHELL_EMPTY_THRESHOLD) {
1789
+ return { isShell: true, reason: "empty-body", visibleTextLength };
1790
+ }
1791
+ return { isShell: false, visibleTextLength };
1792
+ };
1793
+
1794
+ // src/utils/parse-html.ts
1363
1795
  var MAX_ELEMENT_DEPTH = 256;
1364
- var isParent = (node) => "childNodes" in node;
1796
+ var isParent2 = (node) => "childNodes" in node;
1365
1797
  var LINE_ELEMENTS = /* @__PURE__ */ new Set([
1366
1798
  "p",
1367
1799
  "div",
@@ -1402,7 +1834,7 @@ var flattenDeep = (root, maxDepth) => {
1402
1834
  let unwrapped = false;
1403
1835
  for (let index = 0; index < children.length; index++) {
1404
1836
  const child = children[index];
1405
- if (!isParent(child)) {
1837
+ if (!isParent2(child)) {
1406
1838
  continue;
1407
1839
  }
1408
1840
  if (depth + 1 > maxDepth && "tagName" in child) {
@@ -1426,12 +1858,78 @@ var flattenDeep = (root, maxDepth) => {
1426
1858
  }
1427
1859
  }
1428
1860
  };
1429
- var SCRIPT_OR_STYLE = /<(script|style)\b([^>]*)>[\s\S]*?<\/\1\s*>/gi;
1861
+ var SCRIPT_OR_STYLE_START = /<!--|<(script|style)(?=[\s/>])/gi;
1862
+ var CLOSING_TAG = {
1863
+ script: /<\/script[\s/>]/gi,
1864
+ style: /<\/style[\s/>]/gi
1865
+ };
1866
+ var startTagEnd = (html, from) => {
1867
+ let index = from;
1868
+ while (index < html.length) {
1869
+ const char = html[index];
1870
+ if (char === ">") {
1871
+ return index + 1;
1872
+ }
1873
+ if (char === "=") {
1874
+ let value = index + 1;
1875
+ while (value < html.length && /\s/.test(html[value] ?? "")) {
1876
+ value++;
1877
+ }
1878
+ const quote = html[value];
1879
+ if (quote === '"' || quote === "'") {
1880
+ const close = html.indexOf(quote, value + 1);
1881
+ if (close < 0) {
1882
+ return -1;
1883
+ }
1884
+ index = close + 1;
1885
+ continue;
1886
+ }
1887
+ index = value;
1888
+ continue;
1889
+ }
1890
+ index++;
1891
+ }
1892
+ return -1;
1893
+ };
1894
+ var isReadScript = (attributes, element4) => /application\/ld\+json/i.test(attributes) || element4.includes(PLAYER_RESPONSE_VARIABLE) || CHALLENGE_PATTERN.test(element4);
1430
1895
  var LARGE_DOCUMENT_CHARS = 2e6;
1431
- var stripScriptBodies = (html) => html.replace(
1432
- SCRIPT_OR_STYLE,
1433
- (match, tag, attributes) => /application\/ld\+json/i.test(attributes) ? match : `<${tag}${attributes}></${tag}>`
1434
- );
1896
+ var stripScriptBodies = (html) => {
1897
+ const out = [];
1898
+ let copied = 0;
1899
+ const start = new RegExp(SCRIPT_OR_STYLE_START);
1900
+ for (let match = start.exec(html); match; match = start.exec(html)) {
1901
+ if (match[1] === void 0) {
1902
+ const commentEnd = html.indexOf("-->", match.index + 4);
1903
+ if (commentEnd < 0) {
1904
+ break;
1905
+ }
1906
+ start.lastIndex = commentEnd + 3;
1907
+ continue;
1908
+ }
1909
+ const tag = match[1].toLowerCase();
1910
+ const bodyStart = startTagEnd(html, match.index + match[0].length);
1911
+ if (bodyStart < 0) {
1912
+ break;
1913
+ }
1914
+ const closing = CLOSING_TAG[tag];
1915
+ closing.lastIndex = bodyStart;
1916
+ const close = closing.exec(html);
1917
+ if (!close) {
1918
+ break;
1919
+ }
1920
+ const tagEnd = html.indexOf(">", close.index);
1921
+ const elementEnd = tagEnd < 0 ? html.length : tagEnd + 1;
1922
+ const element4 = html.slice(match.index, elementEnd);
1923
+ const openingTag = html.slice(match.index, bodyStart);
1924
+ if (!(tag === "script" && isReadScript(openingTag, element4))) {
1925
+ out.push(html.slice(copied, bodyStart), html.slice(close.index, elementEnd));
1926
+ copied = elementEnd;
1927
+ }
1928
+ start.lastIndex = elementEnd;
1929
+ }
1930
+ out.push(html.slice(copied));
1931
+ return out.join("");
1932
+ };
1435
1933
  var parseHtml = (html, options = {}) => {
1436
1934
  const source = html.length > LARGE_DOCUMENT_CHARS ? stripScriptBodies(html) : html;
1437
1935
  const document2 = options.fragment ? parseFragment(source, { scriptingEnabled: false }) : parse(source, { scriptingEnabled: false });
@@ -1617,246 +2115,24 @@ var hoistAttentionWhitespace = (node) => {
1617
2115
  if (lead) {
1618
2116
  index += spaceBeside(parent.children, index, "before");
1619
2117
  }
1620
- if (trail) {
1621
- spaceBeside(parent.children, index, "after");
1622
- }
1623
- }
1624
- };
1625
- var extractMdast = (node) => {
1626
- const extracted = filter(node, (node2) => {
1627
- if (!emptyDeclarationFilter(node2)) {
1628
- return false;
1629
- }
1630
- return true;
1631
- });
1632
- if (extracted) {
1633
- liftDisplayMathIn(extracted);
1634
- tidyBreaks(extracted);
1635
- hoistAttentionWhitespace(extracted);
1636
- }
1637
- return extracted;
1638
- };
1639
-
1640
- // src/utils/hast-fast.ts
1641
- var EMPTY_METRICS = { text: 0, link: 0, commas: 0, paragraphs: 0, elements: 0 };
1642
- var COMMA_REGEX = /[,、,،]/g;
1643
- var PARAGRAPH_LIKE = /* @__PURE__ */ new Set(["p", "pre", "li", "blockquote", "figcaption", "dd", "dt"]);
1644
- var NON_PROSE_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "svg", "math", "iframe", "object"]);
1645
- var isElement = (node) => typeof node === "object" && node !== null && node.type === "element";
1646
- var isParent2 = (node) => typeof node === "object" && node !== null && Array.isArray(node.children);
1647
- var classList = (element4) => {
1648
- const className = element4.properties?.className;
1649
- if (Array.isArray(className)) {
1650
- return className;
1651
- }
1652
- if (typeof className === "string") {
1653
- return className.split(/\s+/).filter(Boolean);
1654
- }
1655
- return [];
1656
- };
1657
- var matchString = (element4) => {
1658
- const id = element4.properties?.id;
1659
- const classes = classList(element4);
1660
- if (!id && classes.length === 0) {
1661
- return element4.tagName;
1662
- }
1663
- return `${element4.tagName} ${typeof id === "string" ? id : ""} ${classes.join(" ")}`;
1664
- };
1665
- var MetricsCollector = class {
1666
- #cache = /* @__PURE__ */ new WeakMap();
1667
- metrics(node) {
1668
- if (node.type === "text") {
1669
- const value = node.value;
1670
- if (value.trim().length === 0) {
1671
- return EMPTY_METRICS;
1672
- }
1673
- return {
1674
- text: value.length,
1675
- link: 0,
1676
- commas: (value.match(COMMA_REGEX) ?? []).length,
1677
- paragraphs: 0,
1678
- elements: 0
1679
- };
1680
- }
1681
- if (!isParent2(node)) {
1682
- return EMPTY_METRICS;
1683
- }
1684
- const cached = this.#cache.get(node);
1685
- if (cached) {
1686
- return cached;
1687
- }
1688
- if (isElement(node) && NON_PROSE_TAGS.has(node.tagName)) {
1689
- this.#cache.set(node, EMPTY_METRICS);
1690
- return EMPTY_METRICS;
1691
- }
1692
- let text7 = 0;
1693
- let link = 0;
1694
- let commas = 0;
1695
- let paragraphs = 0;
1696
- let elements = 0;
1697
- for (const child of node.children) {
1698
- const childMetrics = this.metrics(child);
1699
- text7 += childMetrics.text;
1700
- link += childMetrics.link;
1701
- commas += childMetrics.commas;
1702
- paragraphs += childMetrics.paragraphs;
1703
- elements += childMetrics.elements;
1704
- if (isElement(child)) {
1705
- elements += 1;
1706
- if (PARAGRAPH_LIKE.has(child.tagName)) {
1707
- paragraphs += 1;
1708
- }
1709
- }
1710
- }
1711
- if (isElement(node) && node.tagName === "a") {
1712
- link = text7;
1713
- }
1714
- const metrics = { text: text7, link, commas, paragraphs, elements };
1715
- this.#cache.set(node, metrics);
1716
- return metrics;
1717
- }
1718
- textLength(node) {
1719
- return this.metrics(node).text;
1720
- }
1721
- /** Fraction of a subtree's text that is link anchor text. `0` for empty subtrees. */
1722
- linkDensity(node) {
1723
- const { text: text7, link } = this.metrics(node);
1724
- if (text7 === 0) {
1725
- return 0;
1726
- }
1727
- return Math.min(1, link / text7);
1728
- }
1729
- };
1730
- var pruneInPlace = (tree, keep) => {
1731
- if (!isParent2(tree)) {
1732
- return tree;
1733
- }
1734
- const stack = [tree];
1735
- while (stack.length > 0) {
1736
- const parent = stack.pop();
1737
- const children = parent.children;
1738
- let write = 0;
1739
- for (let read = 0; read < children.length; read++) {
1740
- const child = children[read];
1741
- if (!keep(child, parent)) {
1742
- continue;
1743
- }
1744
- children[write++] = child;
1745
- if (isParent2(child)) {
1746
- stack.push(child);
1747
- }
1748
- }
1749
- children.length = write;
1750
- }
1751
- return tree;
1752
- };
1753
- var walk = (tree, visit) => {
1754
- const stack = [{ node: tree, parent: void 0 }];
1755
- while (stack.length > 0) {
1756
- const { node, parent } = stack.pop();
1757
- if (visit(node, parent) === false) {
1758
- continue;
1759
- }
1760
- if (isParent2(node)) {
1761
- for (let i = node.children.length - 1; i >= 0; i--) {
1762
- stack.push({ node: node.children[i], parent: node });
1763
- }
1764
- }
1765
- }
1766
- };
1767
- var collectElements = (tree, predicate) => {
1768
- const found = [];
1769
- walk(tree, (node) => {
1770
- if (isElement(node) && predicate(node)) {
1771
- found.push(node);
1772
- }
1773
- });
1774
- return found;
1775
- };
1776
- var findElement = (tree, predicate) => {
1777
- let found;
1778
- walk(tree, (node) => {
1779
- if (found) {
1780
- return false;
1781
- }
1782
- if (isElement(node) && predicate(node)) {
1783
- found = node;
1784
- return false;
1785
- }
1786
- });
1787
- return found;
1788
- };
1789
- var cloneHast = (tree) => {
1790
- try {
1791
- return structuredClone(tree);
1792
- } catch {
1793
- return manualClone(tree);
1794
- }
1795
- };
1796
- var manualClone = (node) => {
1797
- if (Array.isArray(node)) {
1798
- return node.map(manualClone);
1799
- }
1800
- if (typeof node !== "object" || node === null) {
1801
- return node;
1802
- }
1803
- const copy = {};
1804
- for (const [key, value] of Object.entries(node)) {
1805
- copy[key] = manualClone(value);
1806
- }
1807
- return copy;
1808
- };
1809
- var property = (element4, key) => {
1810
- const properties = element4.properties;
1811
- if (!properties) {
1812
- return void 0;
1813
- }
1814
- const direct = properties[key];
1815
- if (direct !== void 0) {
1816
- return direct;
1817
- }
1818
- if (!(key.includes("-") || key.includes(":"))) {
1819
- return void 0;
1820
- }
1821
- return properties[camelCaseAttribute(key)];
1822
- };
1823
- var camelCaseAttribute = (key) => key.replace(/[-:]([a-z])/g, (_, character) => character.toUpperCase());
1824
- var numericProperty = (element4, key) => {
1825
- const raw = property(element4, key);
1826
- if (typeof raw === "number") {
1827
- return Number.isFinite(raw) ? raw : void 0;
1828
- }
1829
- if (typeof raw === "string" && raw.length > 0) {
1830
- const parsed = Number.parseFloat(raw);
1831
- return Number.isFinite(parsed) ? parsed : void 0;
1832
- }
1833
- return void 0;
1834
- };
1835
- var stringProperty = (element4, key) => {
1836
- const raw = property(element4, key);
1837
- if (typeof raw === "string") {
1838
- return raw;
1839
- }
1840
- if (Array.isArray(raw)) {
1841
- return raw.join(" ");
1842
- }
1843
- if (raw === true) {
1844
- return "";
1845
- }
1846
- return void 0;
1847
- };
1848
- var isTruthyAttribute = (element4, key) => {
1849
- const raw = property(element4, key);
1850
- if (raw === void 0 || raw === null || raw === false) {
1851
- return false;
2118
+ if (trail) {
2119
+ spaceBeside(parent.children, index, "after");
2120
+ }
1852
2121
  }
1853
- if (raw === true) {
2122
+ };
2123
+ var extractMdast = (node) => {
2124
+ const extracted = filter(node, (node2) => {
2125
+ if (!emptyDeclarationFilter(node2)) {
2126
+ return false;
2127
+ }
1854
2128
  return true;
2129
+ });
2130
+ if (extracted) {
2131
+ liftDisplayMathIn(extracted);
2132
+ tidyBreaks(extracted);
2133
+ hoistAttentionWhitespace(extracted);
1855
2134
  }
1856
- if (typeof raw === "string") {
1857
- return raw.toLowerCase() !== "false";
1858
- }
1859
- return true;
2135
+ return extracted;
1860
2136
  };
1861
2137
 
1862
2138
  // src/extractors/lib/code-tabs.ts
@@ -3057,8 +3333,8 @@ var DECLARATIVE_ADAPTERS = SPECS.map(defineSelectorAdapter);
3057
3333
  // src/adapters/sites/hackernews.ts
3058
3334
  import { select as select4, selectAll as selectAll2 } from "hast-util-select";
3059
3335
  var MAX_QUOTE_DEPTH = 6;
3060
- var text3 = (value) => ({ type: "text", value });
3061
- var element = (tagName, children) => ({
3336
+ var text4 = (value) => ({ type: "text", value });
3337
+ var element2 = (tagName, children) => ({
3062
3338
  type: "element",
3063
3339
  tagName,
3064
3340
  properties: {},
@@ -3080,7 +3356,7 @@ var commentDepth = (row) => {
3080
3356
  var nest = (content, depth) => {
3081
3357
  let current = content;
3082
3358
  for (let level = 0; level < Math.min(depth, MAX_QUOTE_DEPTH); level++) {
3083
- current = [element("blockquote", current)];
3359
+ current = [element2("blockquote", current)];
3084
3360
  }
3085
3361
  return current[0];
3086
3362
  };
@@ -3088,15 +3364,15 @@ var storyNodes = (root) => {
3088
3364
  const nodes = [];
3089
3365
  const titleLink = select4(".titleline > a", root);
3090
3366
  if (titleLink) {
3091
- nodes.push(element("h1", [...titleLink.children]));
3367
+ nodes.push(element2("h1", [...titleLink.children]));
3092
3368
  const href = stringProperty(titleLink, "href");
3093
3369
  if (href && /^https?:/.test(href)) {
3094
- nodes.push(element("p", [{ ...titleLink, children: [text3(href)] }]));
3370
+ nodes.push(element2("p", [{ ...titleLink, children: [text4(href)] }]));
3095
3371
  }
3096
3372
  }
3097
3373
  const topText = select4(".toptext", root);
3098
3374
  if (topText) {
3099
- nodes.push(element("div", [...topText.children]));
3375
+ nodes.push(element2("div", [...topText.children]));
3100
3376
  }
3101
3377
  return nodes;
3102
3378
  };
@@ -3108,10 +3384,10 @@ var commentNodes = (root) => {
3108
3384
  continue;
3109
3385
  }
3110
3386
  const author = select4(".hnuser", row);
3111
- const header = author ? [element("strong", [...author.children])] : [];
3387
+ const header = author ? [element2("strong", [...author.children])] : [];
3112
3388
  const content = [
3113
- ...header.length > 0 ? [element("p", header)] : [],
3114
- element("div", [...body.children])
3389
+ ...header.length > 0 ? [element2("p", header)] : [],
3390
+ element2("div", [...body.children])
3115
3391
  ];
3116
3392
  nodes.push(nest(content, commentDepth(row)));
3117
3393
  }
@@ -3138,171 +3414,13 @@ var hackernewsAdapter = {
3138
3414
  }
3139
3415
  const children = [...story];
3140
3416
  if (comments.length > 0) {
3141
- children.push(element("h2", [text3("Comments")]));
3417
+ children.push(element2("h2", [text4("Comments")]));
3142
3418
  children.push(...comments);
3143
3419
  }
3144
3420
  return { type: "root", children };
3145
3421
  }
3146
3422
  };
3147
3423
 
3148
- // src/adapters/sites/youtube.ts
3149
- var findObjectEnd = (source, start) => {
3150
- let depth = 0;
3151
- let inString = false;
3152
- let escaped = false;
3153
- for (let index = start; index < source.length; index++) {
3154
- const character = source[index];
3155
- if (escaped) {
3156
- escaped = false;
3157
- } else if (character === "\\") {
3158
- escaped = true;
3159
- } else if (character === '"') {
3160
- inString = !inString;
3161
- } else if (!inString) {
3162
- if (character === "{") {
3163
- depth += 1;
3164
- } else if (character === "}" && --depth === 0) {
3165
- return index + 1;
3166
- }
3167
- }
3168
- }
3169
- return -1;
3170
- };
3171
- var extractAssignedJson = (source, name) => {
3172
- const markerIndex = source.indexOf(`${name} =`);
3173
- if (markerIndex === -1) {
3174
- return void 0;
3175
- }
3176
- const start = source.indexOf("{", markerIndex);
3177
- if (start === -1) {
3178
- return void 0;
3179
- }
3180
- const end = findObjectEnd(source, start);
3181
- if (end === -1) {
3182
- return void 0;
3183
- }
3184
- try {
3185
- return JSON.parse(source.slice(start, end));
3186
- } catch {
3187
- return void 0;
3188
- }
3189
- };
3190
- var scriptText = (root) => collectElements(root, (element4) => element4.tagName === "script").map(
3191
- (script) => script.children.map((child) => child.type === "text" ? child.value : "").join("")
3192
- );
3193
- var findPlayerResponse = (root) => {
3194
- for (const source of scriptText(root)) {
3195
- if (!source.includes("ytInitialPlayerResponse")) {
3196
- continue;
3197
- }
3198
- const parsed = extractAssignedJson(source, "ytInitialPlayerResponse");
3199
- if (parsed && typeof parsed === "object") {
3200
- return parsed;
3201
- }
3202
- }
3203
- return void 0;
3204
- };
3205
- var youtubeCaptionTracks = (root) => {
3206
- const player = findPlayerResponse(root);
3207
- const renderer = player?.captions?.playerCaptionsTracklistRenderer;
3208
- const tracks = renderer?.captionTracks;
3209
- if (!Array.isArray(tracks)) {
3210
- return [];
3211
- }
3212
- return tracks.flatMap((track) => {
3213
- const url = typeof track.baseUrl === "string" ? track.baseUrl : void 0;
3214
- const languageCode = typeof track.languageCode === "string" ? track.languageCode : void 0;
3215
- if (!(url && languageCode)) {
3216
- return [];
3217
- }
3218
- const name = track.name?.simpleText;
3219
- return [
3220
- {
3221
- url,
3222
- languageCode,
3223
- name: typeof name === "string" ? name : void 0,
3224
- isAutomatic: track.kind === "asr"
3225
- }
3226
- ];
3227
- });
3228
- };
3229
- var text4 = (value) => ({ type: "text", value });
3230
- var element2 = (tagName, children) => ({
3231
- type: "element",
3232
- tagName,
3233
- properties: {},
3234
- children
3235
- });
3236
- var heading = (depth, value) => element2(`h${depth}`, [text4(value)]);
3237
- var paragraph = (value) => element2("p", [text4(value)]);
3238
- var descriptionNodes = (description) => description.split(/\n{2,}/).map((block) => block.trim()).filter((block) => block.length > 0).map(
3239
- (block) => element2(
3240
- "p",
3241
- block.split("\n").flatMap((line, index) => index === 0 ? [text4(line)] : [element2("br", []), text4(line)])
3242
- )
3243
- );
3244
- var formatDuration = (seconds) => {
3245
- const hours = Math.floor(seconds / 3600);
3246
- const minutes = Math.floor(seconds % 3600 / 60);
3247
- const remainder = seconds % 60;
3248
- const pad = (value) => String(value).padStart(2, "0");
3249
- return hours > 0 ? `${hours}:${pad(minutes)}:${pad(remainder)}` : `${minutes}:${pad(remainder)}`;
3250
- };
3251
- var metadataList = (details) => {
3252
- const items = [];
3253
- if (details.author) {
3254
- items.push(element2("li", [text4(`Channel: ${details.author}`)]));
3255
- }
3256
- const seconds = Number(details.lengthSeconds);
3257
- if (Number.isFinite(seconds) && seconds > 0) {
3258
- items.push(element2("li", [text4(`Duration: ${formatDuration(seconds)}`)]));
3259
- }
3260
- const views = Number(details.viewCount);
3261
- if (Number.isFinite(views) && views > 0) {
3262
- items.push(element2("li", [text4(`Views: ${views.toLocaleString("en-US")}`)]));
3263
- }
3264
- return items.length > 0 ? element2("ul", items) : void 0;
3265
- };
3266
- var youtubeAdapter = {
3267
- id: "youtube",
3268
- matches: ({ url }) => {
3269
- if (!url) {
3270
- return false;
3271
- }
3272
- try {
3273
- return /(^|\.)(youtube\.com|youtu\.be|youtube-nocookie\.com)$/.test(new URL(url).hostname);
3274
- } catch {
3275
- return false;
3276
- }
3277
- },
3278
- // Scanning every script body for the JSON blob would cost a full traversal on every page, so
3279
- // the fallback keys off markers the signature already collected.
3280
- matchesFingerprint: ({ signature }) => Boolean(signature?.ids.has("movie_player") || signature?.classes.has("ytd-app")),
3281
- extract: ({ hast }) => {
3282
- const player = findPlayerResponse(hast);
3283
- const details = player?.videoDetails;
3284
- if (!details?.title) {
3285
- return void 0;
3286
- }
3287
- const children = [heading(1, details.title)];
3288
- const metadata = metadataList(details);
3289
- if (metadata) {
3290
- children.push(metadata);
3291
- }
3292
- const description = details.shortDescription?.trim();
3293
- if (description) {
3294
- children.push(heading(2, "Description"));
3295
- children.push(...descriptionNodes(description));
3296
- }
3297
- const tracks = youtubeCaptionTracks(hast);
3298
- if (tracks.length > 0) {
3299
- const languages2 = [...new Set(tracks.map((track) => track.languageCode))].sort();
3300
- children.push(paragraph(`Captions available: ${languages2.join(", ")}`));
3301
- }
3302
- return { type: "root", children };
3303
- }
3304
- };
3305
-
3306
3424
  // src/adapters/index.ts
3307
3425
  var BUILTIN_ADAPTERS = [youtubeAdapter, hackernewsAdapter, ...DECLARATIVE_ADAPTERS];
3308
3426
  var MIN_ADAPTER_TEXT = 40;
@@ -4826,6 +4944,8 @@ var NEURAL_MODEL = { "kind": "block-gru", "buckets": 1024, "emb": 16, "hidden":
4826
4944
  var TERMINATOR_PATTERNS = [
4827
4945
  /^was this (page|article|document) helpful/i,
4828
4946
  /^thanks for the feedback/i,
4947
+ // Site-generator credit closing the page ("Powered by Docsify.js", "Powered by GitBook").
4948
+ /^powered by\b/i,
4829
4949
  /^(this page was )?last (modified|updated) (on|at|:)?\s/i,
4830
4950
  /^learn how to contribute/i,
4831
4951
  /^help (us )?improve\b/i,
@@ -4855,7 +4975,7 @@ var textOf3 = (node) => {
4855
4975
  if (node.type === "text") {
4856
4976
  return node.value;
4857
4977
  }
4858
- if (!isParent2(node)) {
4978
+ if (!isParent(node)) {
4859
4979
  return "";
4860
4980
  }
4861
4981
  let text7 = "";
@@ -4873,7 +4993,7 @@ var truncateTrailingBoilerplate = (tree) => {
4873
4993
  total += node.value.trim().length;
4874
4994
  return;
4875
4995
  }
4876
- if (isParent2(node)) {
4996
+ if (isParent(node)) {
4877
4997
  for (const child of node.children) {
4878
4998
  measure(child);
4879
4999
  }
@@ -4886,7 +5006,7 @@ var truncateTrailingBoilerplate = (tree) => {
4886
5006
  const tailStart = total * TAIL_START_FRACTION;
4887
5007
  let cutoff = -1;
4888
5008
  const find = (node) => {
4889
- if (cutoff !== -1 || !isParent2(node)) {
5009
+ if (cutoff !== -1 || !isParent(node)) {
4890
5010
  return;
4891
5011
  }
4892
5012
  for (const child of node.children) {
@@ -5846,11 +5966,10 @@ var languages = [
5846
5966
  ["json", [/\b(true|false|null|\{})\b|\"[^"]+\":/g, 10]],
5847
5967
  ["yaml", [/^(\s+)?[a-z][a-z0-9]*:/gim, 10]]
5848
5968
  ];
5849
- var detectLanguage = (code) => {
5850
- return languages.map(
5851
- ([lang, ...features]) => [lang, features.reduce((acc, [match, score]) => acc + [...code.matchAll(match)].length * score, 0)]
5852
- ).filter(([_, score]) => score > 20).sort((a, b) => b[1] - a[1])[0]?.[0] || "plain";
5853
- };
5969
+ var MIN_CONFIDENT_SCORE = 400;
5970
+ var detectLanguage = (code) => languages.map(
5971
+ ([lang, ...features]) => [lang, features.reduce((acc, [match, score]) => acc + [...code.matchAll(match)].length * score, 0)]
5972
+ ).filter(([_, score]) => score >= MIN_CONFIDENT_SCORE).sort((a, b) => b[1] - a[1])[0]?.[0];
5854
5973
 
5855
5974
  // src/utils/pre-text.ts
5856
5975
  var LINE_BLOCK_TAGS = /* @__PURE__ */ new Set([
@@ -5977,17 +6096,46 @@ var errorLine = (error) => {
5977
6096
 
5978
6097
  // src/mdast-handlers/custom-code-handler.ts
5979
6098
  var LANGUAGE_MATCH_REGEX = [/^language-(\S+)$/, /^highlight-source-(\S+)$/, /^CodeBlock--language-(\S+)$/];
6099
+ var LANGUAGE_TOKEN = /^[\w+#.-]+$/;
6100
+ var NO_LANGUAGE = /* @__PURE__ */ new Set(["plain", "plaintext", "text", "none", "nohighlight"]);
5980
6101
  var LANGUAGE_ATTRIBUTES = ["dataLanguage", "dataLang"];
5981
6102
  var attributeLanguage = (element4) => {
5982
6103
  for (const name of LANGUAGE_ATTRIBUTES) {
5983
6104
  const value = element4.properties?.[name];
5984
- if (typeof value === "string" && /^[\w+#.-]+$/.test(value.trim()) && value.trim() !== "plaintext") {
6105
+ if (typeof value === "string" && LANGUAGE_TOKEN.test(value.trim()) && value.trim() !== "plaintext") {
5985
6106
  return value.trim();
5986
6107
  }
5987
6108
  }
5988
6109
  return void 0;
5989
6110
  };
5990
- var codeLanguage = (...elements) => elements.flatMap(classList).map((className) => LANGUAGE_MATCH_REGEX.map((regex) => className.match(regex)).find(Boolean)?.[1]).find(Boolean) ?? elements.map(attributeLanguage).find(Boolean);
6111
+ var SANDPACK_LANGUAGES = /* @__PURE__ */ new Set([
6112
+ "javascript",
6113
+ "typescript",
6114
+ "jsx",
6115
+ "tsx",
6116
+ "html",
6117
+ "css",
6118
+ "less",
6119
+ "scss",
6120
+ "sass",
6121
+ "json",
6122
+ "markdown",
6123
+ "python",
6124
+ "vue",
6125
+ "svelte"
6126
+ ]);
6127
+ var conventionLanguage = (element4) => {
6128
+ const classes = classList(element4);
6129
+ const brushAt = classes.findIndex((name) => name.startsWith("brush:"));
6130
+ if (brushAt >= 0) {
6131
+ const brush = classes[brushAt] === "brush:" ? classes[brushAt + 1] : classes[brushAt].slice("brush:".length);
6132
+ const lang = brush?.toLowerCase();
6133
+ return lang && LANGUAGE_TOKEN.test(lang) && !NO_LANGUAGE.has(lang) ? lang : void 0;
6134
+ }
6135
+ const sandpack = classes.find((name) => name.startsWith("sp-") && SANDPACK_LANGUAGES.has(name.slice(3)));
6136
+ return sandpack?.slice(3);
6137
+ };
6138
+ var codeLanguage = (...elements) => elements.flatMap(classList).map((className) => LANGUAGE_MATCH_REGEX.map((regex) => className.match(regex)).find(Boolean)?.[1]).find(Boolean) ?? elements.map(attributeLanguage).find(Boolean) ?? elements.map(conventionLanguage).find(Boolean);
5991
6139
  var codeMeta = (pre) => {
5992
6140
  const title = pre.properties?.dataTitle;
5993
6141
  if (typeof title !== "string") {
@@ -6568,6 +6716,82 @@ var removeIconFonts = (tree) => {
6568
6716
  pruneInPlace(tree, (node) => !isIcon(node));
6569
6717
  }
6570
6718
  };
6719
+ var textOfNode = (node) => node.type === "text" ? node.value : "children" in node ? node.children.map(textOfNode).join("") : "";
6720
+ var unwrapHeadingAnchors = (tree) => {
6721
+ walk(tree, (node) => {
6722
+ if (!(isElement(node) && /^h[1-6]$/.test(node.tagName))) {
6723
+ return;
6724
+ }
6725
+ const title = textOfNode(node).trim();
6726
+ if (title === "") {
6727
+ return;
6728
+ }
6729
+ const anchorAt = node.children.findIndex(
6730
+ (child) => isElement(child) && child.tagName === "a" && stringProperty(child, "href")?.startsWith("#") && textOfNode(child).trim() === title
6731
+ );
6732
+ const anchor = node.children[anchorAt];
6733
+ if (isElement(anchor)) {
6734
+ node.children.splice(anchorAt, 1, ...anchor.children);
6735
+ }
6736
+ });
6737
+ };
6738
+ var LABEL_LANGUAGES = /* @__PURE__ */ new Set([
6739
+ "js",
6740
+ "javascript",
6741
+ "ts",
6742
+ "typescript",
6743
+ "jsx",
6744
+ "tsx",
6745
+ "html",
6746
+ "css",
6747
+ "scss",
6748
+ "json",
6749
+ "bash",
6750
+ "sh",
6751
+ "shell",
6752
+ "console",
6753
+ "python",
6754
+ "py",
6755
+ "sql",
6756
+ "xml",
6757
+ "yaml",
6758
+ "toml",
6759
+ "http",
6760
+ "rust",
6761
+ "go",
6762
+ "java",
6763
+ "kotlin",
6764
+ "swift",
6765
+ "c",
6766
+ "cpp",
6767
+ "md",
6768
+ "markdown",
6769
+ "diff"
6770
+ ]);
6771
+ var LABEL_NO_LANGUAGE = /* @__PURE__ */ new Set(["plain", "plaintext", "text"]);
6772
+ var foldCodeLabels = (tree) => {
6773
+ walk(tree, (node) => {
6774
+ if (!isElement(node) || node.tagName === "pre") {
6775
+ return;
6776
+ }
6777
+ const elements = node.children.filter(isElement);
6778
+ if (elements.length !== 2 || node.children.some((child) => child.type === "text" && child.value.trim() !== "")) {
6779
+ return;
6780
+ }
6781
+ const [label, pre] = elements;
6782
+ if (pre.tagName !== "pre" || /^(h[1-6]|pre)$/.test(label.tagName) || findElement(label, (el) => el.tagName === "pre")) {
6783
+ return;
6784
+ }
6785
+ const name = textOfNode(label).trim().toLowerCase();
6786
+ if (!(LABEL_LANGUAGES.has(name) || LABEL_NO_LANGUAGE.has(name))) {
6787
+ return;
6788
+ }
6789
+ if (LABEL_LANGUAGES.has(name) && pre.properties.dataLanguage === void 0) {
6790
+ pre.properties.dataLanguage = name;
6791
+ }
6792
+ node.children = node.children.filter((child) => child !== label);
6793
+ });
6794
+ };
6571
6795
  var SPACED_INLINE = /* @__PURE__ */ new Set(["span", "time", "label"]);
6572
6796
  var SCRIPTS = [
6573
6797
  ["latin", /\p{Script=Latin}/u],
@@ -6584,7 +6808,6 @@ var needsSpace = (left, right) => {
6584
6808
  return a !== void 0 && b !== void 0 && a !== b;
6585
6809
  };
6586
6810
  var NO_SPACING = /* @__PURE__ */ new Set(["pre", "code", "kbd", "samp", "ruby", "math", "svg"]);
6587
- var textOfNode = (node) => node.type === "text" ? node.value : "children" in node ? node.children.map(textOfNode).join("") : "";
6588
6811
  var separateAdjacentInlines = (tree) => {
6589
6812
  const visit = (node) => {
6590
6813
  if (!("children" in node) || isElement(node) && NO_SPACING.has(node.tagName)) {
@@ -6605,7 +6828,15 @@ var separateAdjacentInlines = (tree) => {
6605
6828
  visit(tree);
6606
6829
  };
6607
6830
  var normalizeHast = (tree, options = {}) => {
6608
- const { images = true, math = true, ariaHeadings = true, iconFonts = true, inlineSpacing = true } = options;
6831
+ const {
6832
+ images = true,
6833
+ math = true,
6834
+ ariaHeadings = true,
6835
+ iconFonts = true,
6836
+ headingAnchors = true,
6837
+ codeLabels = true,
6838
+ inlineSpacing = true
6839
+ } = options;
6609
6840
  if (images) {
6610
6841
  resolveImageSources(tree);
6611
6842
  }
@@ -6618,6 +6849,12 @@ var normalizeHast = (tree, options = {}) => {
6618
6849
  if (iconFonts) {
6619
6850
  removeIconFonts(tree);
6620
6851
  }
6852
+ if (headingAnchors) {
6853
+ unwrapHeadingAnchors(tree);
6854
+ }
6855
+ if (codeLabels) {
6856
+ foldCodeLabels(tree);
6857
+ }
6621
6858
  if (inlineSpacing) {
6622
6859
  separateAdjacentInlines(tree);
6623
6860
  }
@@ -7030,55 +7267,6 @@ var mentionsTitle = (opening, title) => {
7030
7267
  };
7031
7268
  var normalizeForCompare = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
7032
7269
 
7033
- // src/detect-client-shell.ts
7034
- var SHELL_SUSPICION_THRESHOLD = 300;
7035
- var SHELL_EMPTY_THRESHOLD = 50;
7036
- var BODY_PATTERN = /<body[^>]*>([\s\S]*?)<\/body>/i;
7037
- var HEAD_PATTERN = /<head[^>]*>[\s\S]*?<\/head>/gi;
7038
- var NOSCRIPT_PATTERN = /<noscript[^>]*>([\s\S]*?)<\/noscript>/gi;
7039
- var INVISIBLE_ELEMENT_PATTERN = /<(script|style|template|svg|noscript)[^>]*>[\s\S]*?<\/\1>/gi;
7040
- var COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
7041
- var TAG_PATTERN = /<[^>]+>/g;
7042
- var MOUNT_POINT_PATTERN = /<[a-z][^>]*\sid=["'](?:root|app|__next|___gatsby|__nuxt|q-app|svelte|react-root)["']|<app-root[\s>]|\sdata-reactroot[\s>=]/i;
7043
- var CHALLENGE_PATTERN = /cf-chl|challenge-platform|cf_chl_opt|just a moment|attention required|checking your browser|_incapsula_|px-captcha|datadome/i;
7044
- var ENTITIES = {
7045
- "&nbsp;": " ",
7046
- "&amp;": "&",
7047
- "&lt;": "<",
7048
- "&gt;": ">",
7049
- "&quot;": '"',
7050
- "&#39;": "'"
7051
- };
7052
- var visibleTextOf = (markup) => markup.replace(COMMENT_PATTERN, " ").replace(INVISIBLE_ELEMENT_PATTERN, " ").replace(TAG_PATTERN, " ").replace(/&(?:nbsp|amp|lt|gt|quot|#39);/g, (entity) => ENTITIES[entity] ?? " ").replace(/\s+/g, " ").trim();
7053
- var noscriptMentionsJavascript = (html) => {
7054
- for (const match of html.matchAll(NOSCRIPT_PATTERN)) {
7055
- if (/javascript/i.test(match[1] ?? "")) {
7056
- return true;
7057
- }
7058
- }
7059
- return false;
7060
- };
7061
- var detectClientShell = (html) => {
7062
- const body = BODY_PATTERN.exec(html)?.[1] ?? html.replace(HEAD_PATTERN, " ");
7063
- const visibleTextLength = visibleTextOf(body).length;
7064
- if (visibleTextLength >= SHELL_SUSPICION_THRESHOLD) {
7065
- return { isShell: false, visibleTextLength };
7066
- }
7067
- if (CHALLENGE_PATTERN.test(html)) {
7068
- return { isShell: true, reason: "anti-bot-challenge", visibleTextLength };
7069
- }
7070
- if (MOUNT_POINT_PATTERN.test(body)) {
7071
- return { isShell: true, reason: "spa-shell", visibleTextLength };
7072
- }
7073
- if (noscriptMentionsJavascript(html)) {
7074
- return { isShell: true, reason: "noscript-only", visibleTextLength };
7075
- }
7076
- if (visibleTextLength < SHELL_EMPTY_THRESHOLD) {
7077
- return { isShell: true, reason: "empty-body", visibleTextLength };
7078
- }
7079
- return { isShell: false, visibleTextLength };
7080
- };
7081
-
7082
7270
  // src/extract-meta-refresh.ts
7083
7271
  var MAX_META_REFRESH_DELAY_SECONDS = 10;
7084
7272
  var META_TAG_PATTERN = /<meta\b[^>]*>/gi;