@mark1russell7/site-pages 0.1.3 → 0.1.5

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.
@@ -69,7 +69,7 @@ function versionOfTag(tag) {
69
69
  const VERSION_HEADING = /^##\s+\[?v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\]?(?:\s.*)?$/;
70
70
  const COMMIT_ENTRY = /^([0-9a-f]{7,40}):\s+([\s\S]*)$/;
71
71
  const DEPENDENCY_ENTRY = /^(?:Updated dependencies\b|@?[\w.-]+(?:\/[\w.-]+)?@\S+\s*$)/;
72
- const GENERATED_NOTE = /\s+by\s+@[\w-]+(?:\[bot\])?\s+in\s+(https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/(\d+))\s*$/;
72
+ const GENERATED_NOTE = /\sby\s+@[\w-]+(?:\[bot\])?\s+in\s+(https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/(\d+))\s*$/;
73
73
  const FIRST_CONTRIBUTION = /\bmade their first contribution\b/;
74
74
  /**
75
75
  * This function gives the first sentence of a text. A full stop, a question mark or an exclamation
@@ -108,7 +108,8 @@ function entryOf(lines) {
108
108
  const commit = COMMIT_ENTRY.exec(text);
109
109
  let body = (commit ? commit[2] : text) ?? "";
110
110
  if (body.trim() === "" || DEPENDENCY_ENTRY.test(body.trim()) || FIRST_CONTRIBUTION.test(body)) return null;
111
- body = body.replace(GENERATED_NOTE, (_match, url, number) => ` ([#${number}](${url}))`);
111
+ const note = GENERATED_NOTE.exec(body);
112
+ if (note) body = `${body.slice(0, note.index).trimEnd()} ([#${note[2] ?? ""}](${note[1] ?? ""}))`;
112
113
  return {
113
114
  commit: commit?.[1] ?? null,
114
115
  text: firstSentence(body)
@@ -179,10 +180,10 @@ function releaseBodyEntries(body) {
179
180
  continue;
180
181
  }
181
182
  if (fence !== null) continue;
182
- const item = /^[-*+]\s+(.*)$/.exec(line);
183
- if (item?.[1] !== void 0) {
183
+ const item = /^[-*+]\s+(\S.*)?$/.exec(line);
184
+ if (item) {
184
185
  close();
185
- entry = [item[1]];
186
+ entry = [item[1] ?? ""];
186
187
  } else if (entry && (line.trim() === "" || /^\s/.test(line))) entry.push(line);
187
188
  else close();
188
189
  }
@@ -475,7 +476,7 @@ async function npmPublishTimes(options) {
475
476
  const queue = [...names].sort();
476
477
  const worker = async () => {
477
478
  for (let name = queue.shift(); name !== void 0; name = queue.shift()) try {
478
- const packument = object(await get(`${registry}/${name.replace("/", "%2f")}`));
479
+ const packument = object(await get(`${registry}/${name.replaceAll("/", "%2f")}`));
479
480
  const published = object(packument["versions"]);
480
481
  for (const [version, date] of Object.entries(object(packument["time"]))) {
481
482
  if (version === "created" || version === "modified" || typeof date !== "string") continue;
@@ -986,13 +987,78 @@ function splitRow(line) {
986
987
  cells.push(cell.trim());
987
988
  return cells;
988
989
  }
990
+ /** A whitespace character, as `\s` of a regular expression and `String.prototype.trim` read it. */
991
+ const SPACE = /\s/;
992
+ /** A character that `.` of a regular expression does not match. */
993
+ const LINE_TERMINATOR = /[\n\r\u2028\u2029]/;
994
+ /** The first whitespace character at or after `lastIndex`. */
995
+ const NEXT_SPACE = /\s/g;
996
+ /**
997
+ * This function replaces each Markdown link `[label](target)` of a text with the result of
998
+ * `replace`. The label ends at the first `]`, and the target ends at the first `)`. In `strict`
999
+ * mode, the label and the target are not empty, and the target has no whitespace.
1000
+ *
1001
+ * @remarks The function reads the text one time. A regular expression for these links can
1002
+ * backtrack (CodeQL `js/polynomial-redos`). For a long text with many `[` characters, its time
1003
+ * grows with the square of the length.
1004
+ */
1005
+ function replaceLinks(text, strict, replace) {
1006
+ let result = "";
1007
+ let copied = 0;
1008
+ let close = -1;
1009
+ let end = -1;
1010
+ let space = -1;
1011
+ for (let open = text.indexOf("["); open !== -1; open = text.indexOf("[", open + 1)) {
1012
+ if (close <= open) close = text.indexOf("]", open + 1);
1013
+ if (close === -1) break;
1014
+ if (text[close + 1] !== "(" || strict && close === open + 1) continue;
1015
+ const start = close + 2;
1016
+ if (end < start) end = text.indexOf(")", start);
1017
+ if (end === -1) break;
1018
+ if (strict) {
1019
+ if (space !== Infinity && space < start) {
1020
+ NEXT_SPACE.lastIndex = start;
1021
+ space = NEXT_SPACE.exec(text)?.index ?? Infinity;
1022
+ }
1023
+ if (end === start || space < end) continue;
1024
+ }
1025
+ result += text.slice(copied, open) + replace(text.slice(open + 1, close), text.slice(start, end));
1026
+ copied = end + 1;
1027
+ open = end;
1028
+ }
1029
+ return result + text.slice(copied);
1030
+ }
1031
+ /**
1032
+ * This function gives the text of an ATX heading without its closing sequence: the whitespace,
1033
+ * then the `#` characters, then the whitespace at the end. It gives null when the rest has a line
1034
+ * terminator, as the regular expression `(.*?)\s*#*\s*$` does.
1035
+ */
1036
+ function closeHeading(text) {
1037
+ let end = text.length;
1038
+ while (end > 0 && SPACE.test(text[end - 1] ?? "")) end--;
1039
+ while (end > 0 && text[end - 1] === "#") end--;
1040
+ while (end > 0 && SPACE.test(text[end - 1] ?? "")) end--;
1041
+ const rest = text.slice(0, end);
1042
+ return LINE_TERMINATOR.test(rest) ? null : rest;
1043
+ }
1044
+ /** The start of an ATX heading. The text after the whitespace goes to `closeHeading`. */
1045
+ const HEADING = /^(#{1,6})\s+(\S[\s\S]*)?$/;
1046
+ /** This function gives the level and the text of an ATX heading line, or null for another line. */
1047
+ function headingOf(line) {
1048
+ const match = HEADING.exec(line);
1049
+ const text = match ? closeHeading(match[2] ?? "") : null;
1050
+ return match && text !== null ? {
1051
+ level: match[1]?.length ?? 0,
1052
+ text
1053
+ } : null;
1054
+ }
989
1055
  /** This function gives the plain text of inline Markdown: no code marks, no emphasis, the text of each link. */
990
1056
  function plainText(markdown) {
991
- return String(markdown ?? "").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[`*_]/g, "").replace(/\s+/g, " ").trim();
1057
+ return replaceLinks(String(markdown ?? ""), false, (label) => label).replace(/[`*_]/g, "").replace(/\s+/g, " ").trim();
992
1058
  }
993
1059
  /** A cell with no value: empty, or a dash. */
994
1060
  function isEmpty(cell) {
995
- return cell === void 0 || /^\s*(?:[-–—]|n\/a)?\s*$/i.test(cell);
1061
+ return cell === void 0 || /^(?:[-–—]|n\/a)?$/i.test(cell.trim());
996
1062
  }
997
1063
  /**
998
1064
  * This function gives the tables of a Markdown file. Each table has its header names in lowercase
@@ -1012,14 +1078,14 @@ function markdownTables(markdown) {
1012
1078
  continue;
1013
1079
  }
1014
1080
  if (fence !== null) continue;
1015
- const heading = /^#{1,6}\s+(.*?)\s*#*\s*$/.exec(line);
1081
+ const heading = headingOf(line);
1016
1082
  if (heading) {
1017
- section = plainText(heading[1]);
1083
+ section = plainText(heading.text);
1018
1084
  continue;
1019
1085
  }
1020
1086
  if (!line.trim().startsWith("|")) continue;
1021
1087
  const separator = lines[index + 1] ?? "";
1022
- if (!/^\s*\|?\s*:?-{3,}/.test(separator)) continue;
1088
+ if (!/^\s*(?:\|\s*)?:?-{3,}/.test(separator)) continue;
1023
1089
  const header = splitRow(line).map((name) => plainText(name).toLowerCase());
1024
1090
  const rows = [];
1025
1091
  let next = index + 2;
@@ -1059,6 +1125,20 @@ function linkUrl(target, { file = "", repository = "", ref = "HEAD" }) {
1059
1125
  return `https://github.com/${repository}/blob/${ref}/${resolved}${anchor ? `#${anchor}` : ""}`;
1060
1126
  }
1061
1127
  /**
1128
+ * This function gives the word right before the whitespace that ends `text.slice(0, end)`, as the
1129
+ * regular expression `([A-Za-z][\w.-]*)\s+$` does. The word starts with a letter. The function
1130
+ * reads only the characters of the word and of the whitespace.
1131
+ */
1132
+ function wordBefore(text, end) {
1133
+ let stop = end;
1134
+ while (stop > 0 && SPACE.test(text[stop - 1] ?? "")) stop--;
1135
+ if (stop === end) return void 0;
1136
+ let start = stop;
1137
+ while (start > 0 && /[\w.-]/.test(text[start - 1] ?? "")) start--;
1138
+ while (start < stop && !/[A-Za-z]/.test(text[start] ?? "")) start++;
1139
+ return start < stop ? text.slice(start, stop) : void 0;
1140
+ }
1141
+ /**
1062
1142
  * This function gives the links of a cell: each Markdown link, then each bare reference in the rest
1063
1143
  * of the text. A reference is a pull request ("PR #12", "#12") or a commit hash in a code span.
1064
1144
  *
@@ -1077,7 +1157,7 @@ function linksOf(cell, options = {}) {
1077
1157
  url
1078
1158
  });
1079
1159
  };
1080
- const rest = String(cell ?? "").replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_match, label, target) => {
1160
+ const rest = replaceLinks(String(cell ?? ""), true, (label, target) => {
1081
1161
  add(plainText(label), linkUrl(target, options));
1082
1162
  return " ";
1083
1163
  });
@@ -1087,7 +1167,7 @@ function linksOf(cell, options = {}) {
1087
1167
  let pulls = false;
1088
1168
  const reference = /\bPRs?\s*#(\d+)|(?<![\w/&])#(\d+)\b|`([0-9a-f]{7,40})`/g;
1089
1169
  for (let match = reference.exec(rest); match; match = reference.exec(rest)) {
1090
- const word = /([A-Za-z][\w.-]*)\s+$/.exec(rest.slice(0, match.index))?.[1];
1170
+ const word = wordBefore(rest, match.index);
1091
1171
  if (word && !NOT_REPOSITORIES.has(word.toLowerCase())) current = word;
1092
1172
  const prefix = current === defaultRepo ? "" : `${current} `;
1093
1173
  if (match[1] !== void 0) pulls = true;
@@ -1097,15 +1177,30 @@ function linksOf(cell, options = {}) {
1097
1177
  }
1098
1178
  return links;
1099
1179
  }
1100
- /** The heading of an item: "### `id`: Title". A dash can take the place of the colon. */
1101
- const ITEM_HEADING = /^###\s+`([^`]+)`\s*[:–—-]\s*(.*?)\s*#*\s*$/;
1180
+ /**
1181
+ * The heading of an item: "### `id`: Title". A dash can take the place of the colon. The title goes
1182
+ * to `closeHeading`.
1183
+ */
1184
+ const ITEM_HEADING = /^###\s+`([^`]+)`\s*[:–—-]\s*(\S[\s\S]*)?$/;
1102
1185
  /** The other heading of an item: "### S1 · Title". The ID is one word before the middle dot. */
1103
- const ITEM_HEADING_DOT = /^###\s+([A-Za-z0-9][\w.-]*)\s+·\s+(.*?)\s*#*\s*$/;
1186
+ const ITEM_HEADING_DOT = /^###\s+([A-Za-z0-9][\w.-]*)\s+·\s+(\S[\s\S]*)?$/;
1187
+ /** This function gives the ID and the title of an item heading, or null for another line. */
1188
+ function itemHeadingOf(line) {
1189
+ for (const pattern of [ITEM_HEADING, ITEM_HEADING_DOT]) {
1190
+ const match = pattern.exec(line);
1191
+ const title = match ? closeHeading(match[2] ?? "") : null;
1192
+ if (match && title !== null) return {
1193
+ id: match[1] ?? "",
1194
+ title
1195
+ };
1196
+ }
1197
+ return null;
1198
+ }
1104
1199
  /**
1105
1200
  * A field of an item heading: "- Area: Docs", "- **Area:** Docs" or "- **Area**: Docs". The label
1106
1201
  * matches without case.
1107
1202
  */
1108
- const ITEM_FIELD = /^\s*[-*]\s+(\*\*|__)?([A-Za-z][A-Za-z ]*?)(?::\1|\1:)\s*(.*)$/;
1203
+ const ITEM_FIELD = /^\s*[-*]\s+(\*\*|__)?([A-Za-z][A-Za-z ]*?)(?::\1|\1:)\s*(\S.*)?$/;
1109
1204
  /** The fields of an item heading that give the optional fields of an item. */
1110
1205
  const ITEM_FIELDS = {
1111
1206
  area: "area",
@@ -1138,20 +1233,20 @@ function headingItems(markdown, options) {
1138
1233
  continue;
1139
1234
  }
1140
1235
  if (fence !== null) continue;
1141
- const heading = /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line);
1236
+ const heading = headingOf(line);
1142
1237
  if (heading) {
1143
- const level = heading[1]?.length ?? 0;
1238
+ const { level } = heading;
1144
1239
  if (level > 3) continue;
1145
1240
  current = void 0;
1146
- if (level === 2) section = plainText(heading[2]);
1241
+ if (level === 2) section = plainText(heading.text);
1147
1242
  if (level === 1) section = "";
1148
1243
  if (level !== 3) continue;
1149
- const item = ITEM_HEADING.exec(line) ?? ITEM_HEADING_DOT.exec(line);
1150
- const id = plainText(item?.[1]);
1244
+ const item = itemHeadingOf(line);
1245
+ const id = plainText(item?.id);
1151
1246
  if (!item || isEmpty(id)) continue;
1152
1247
  current = {
1153
1248
  id,
1154
- title: item[2] ?? "",
1249
+ title: item.title,
1155
1250
  status: section,
1156
1251
  statusClass: statusClassOf(section),
1157
1252
  section,
@@ -1310,13 +1405,29 @@ function escapeHtml(text) {
1310
1405
  return String(text).replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
1311
1406
  }
1312
1407
  /**
1408
+ * This function removes the control characters and the spaces (U+0000 to U+0020) at the two ends
1409
+ * of a text. A loop does it, because the regular expression `[\u0000- ]+$` backtracks. For a long
1410
+ * run of these characters, its time grows with the square of the length.
1411
+ */
1412
+ function trimControl(text) {
1413
+ let start = 0;
1414
+ let end = text.length;
1415
+ while (start < end && text.charCodeAt(start) <= 32) start++;
1416
+ while (end > start && text.charCodeAt(end - 1) <= 32) end--;
1417
+ return text.slice(start, end);
1418
+ }
1419
+ /**
1313
1420
  * This function gives a URL that a link can hold, or null. It accepts `http:`, `https:` and
1314
1421
  * `mailto:` URLs, and relative URLs. It refuses each other scheme, for example `javascript:`.
1422
+ *
1423
+ * The URL parser of a browser removes each tab and line break, and the control characters and
1424
+ * spaces at the two ends. This function removes them before the scheme check, as the browser does.
1425
+ * Thus a control character before `javascript:` does not hide the scheme.
1315
1426
  */
1316
1427
  function safeUrl(url) {
1317
- const trimmed = url.trim();
1428
+ const trimmed = trimControl(url.trim().replace(/[\t\n\r]/g, ""));
1318
1429
  if (/^(?:https?:|mailto:)/i.test(trimmed)) return trimmed;
1319
- if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) || trimmed.startsWith("//")) return null;
1430
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) || /^[/\\]{2}/.test(trimmed)) return null;
1320
1431
  return trimmed;
1321
1432
  }
1322
1433
  /** This function gives an `<a>` element, or the escaped text when the URL is not safe. */
@@ -1914,7 +2025,11 @@ document.addEventListener("click", (event) => {
1914
2025
  const label = button.querySelector("[data-sp-label]");
1915
2026
  if (label) label.textContent = open ? button.dataset.less : button.dataset.more;
1916
2027
  });
1917
- const hashed = decodeURIComponent(location.hash.slice(1));
2028
+ // A malformed hash, for example "#100%", must not stop the module
2029
+ let hashed = "";
2030
+ try {
2031
+ hashed = decodeURIComponent(location.hash.slice(1));
2032
+ } catch {}
1918
2033
  for (const [version, panel] of panels) if (panel.id === hashed) select(version, null);
1919
2034
  const revealAll = () => { for (const box of boxes) reveal(box, selected, false); };
1920
2035
  revealAll();
@@ -2000,7 +2115,7 @@ function statusLead(data) {
2000
2115
  }
2001
2116
  /** This function gives the part of a URL for a text: "The server stack (R37)" gives "the-server-stack-r37". */
2002
2117
  function slugOf(text) {
2003
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "other";
2118
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "other";
2004
2119
  }
2005
2120
  /** This function gives the sections of the items, in the sequence of the file. */
2006
2121
  function sectionsOf(items) {
@@ -2420,4 +2535,4 @@ async function runCli(argv, io) {
2420
2535
  //#endregion
2421
2536
  export { GITHUB_API as $, RoadmapParseError as A, DEFAULT_CHANGELOGS as B, longDate as C, inlineMarkdown as D, escapeHtml as E, plainText as F, globFiles as G, mergeReleases as H, readStatusBoard as I, headingDate as J, gitTagDates as K, roadmapItems as L, buildStatusBoard as M, linksOf as N, safeUrl as O, markdownTables as P, npmPublishTimes as Q, splitRow as R, dayRange as S, utcDay as T, readChangelogs as U, ReleaseDataError as V, readReleases as W, originRepository as X, isShallow as Y, NPM_REGISTRY as Z, oldestFirst as _, runCli as a, headingAnchor as at, BASE_CSS as b, openGroups as c, compareVersions as ct, slugOf as d, versionOfTag as dt, GITHUB_GRAPHQL as et, statusLead as f, laneCenter as g, renderReleasesPage as h, parseBuildArgs as i, firstSentence as it, STATUS_CLASSES as j, OPEN_CLASSES as k, renderStatusPage as l, kindOf as lt, releasesLead as m, UsageError as n, githubOpenWork as nt, STATUS_SCRIPT as o, parseChangelog as ot, RELEASES_SCRIPT as p, headCommit as q, build as r, githubReleases as rt, classCounts as s, releaseBodyEntries as st, HELP as t, checkStateOf as tt, sectionsOf as u, parseVersion as ut, releaseSummary as v, shortDate as w, pageShell as x, timelineLayout as y, statusClassOf as z };
2422
2537
 
2423
- //# sourceMappingURL=build-CArquriz.js.map
2538
+ //# sourceMappingURL=build-MO5a6V2k.js.map