@bitcobblers/wod-wiki-core 0.35.2116 → 0.35.2117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -483,6 +483,7 @@ var bySpelling = (() => {
483
483
  return map;
484
484
  })();
485
485
  function lookupUnit(spelling) {
486
+ if (typeof spelling !== "string") return void 0;
486
487
  return bySpelling.get(spelling.trim().toLowerCase());
487
488
  }
488
489
  function dimensionKeyOf(spelling) {
@@ -976,395 +977,248 @@ var toNotebookTag = (notebookId) => `${NOTEBOOK_TAG_PREFIX}${notebookId}`;
976
977
  var fromNotebookTag = (tag) => tag.startsWith(NOTEBOOK_TAG_PREFIX) ? tag.slice(NOTEBOOK_TAG_PREFIX.length) : null;
977
978
  var isNotebookTag = (tag) => tag.startsWith(NOTEBOOK_TAG_PREFIX);
978
979
 
979
- // src/sections/blockDetection.ts
980
+ // src/sections/sectionParser.ts
981
+ var KNOWN_SECTION_WIDGETS = ["playground-run-tip", "attention", "code-example", "syntax-group"];
980
982
  function matchDialectFence(trimmedLine) {
981
- if (!trimmedLine.startsWith("```")) return null;
982
- const tag = trimmedLine.slice(3).split(/[\s\t]/)[0].toLowerCase();
983
- const [base, sport] = tag.split(":", 2);
984
- if (VALID_FENCE_DIALECTS.includes(base)) {
985
- return { dialect: base, sport: sport || void 0 };
986
- }
987
- return null;
983
+ const match = trimmedLine.match(/^`{3,}(time|log)(?::([\w-]+))?\s*$/i);
984
+ if (!match) return null;
985
+ return { dialect: match[1].toLowerCase() === "time" ? "time" : "log", ...match[2] ? { sport: match[2] } : {} };
988
986
  }
989
- function detectScriptBlocks(content) {
990
- const lines = content.split("\n");
991
- const blocks = [];
992
- let inBlock = false;
993
- let currentBlock = {};
994
- let blockContent = [];
995
- lines.forEach((line, index) => {
996
- const trimmedLine = line.trim();
997
- if (!inBlock) {
998
- const fence = matchDialectFence(trimmedLine);
999
- if (fence) {
1000
- inBlock = true;
1001
- const now = Date.now();
1002
- currentBlock = {
1003
- id: `${fence.dialect}-block-${now}-${Math.random().toString(36).slice(2, 9)}`,
1004
- dialect: fence.dialect,
1005
- sport: fence.sport,
1006
- startLine: index,
987
+ function frontmatterSubtype(properties) {
988
+ const type = properties.type?.toLowerCase();
989
+ if (type === "youtube" || type === "strava" || type === "amazon" || type === "file" || type === "effort") return type;
990
+ const url = properties.url || properties.link || "";
991
+ const host = url.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").split(/[/?#:]/, 1)[0].toLowerCase();
992
+ if (host === "youtu.be" || host === "youtube.com" || host.endsWith(".youtube.com")) return "youtube";
993
+ if (host === "strava.com" || host.endsWith(".strava.com")) return "strava";
994
+ if (host === "amzn.to" || host === "amazon.com" || host.endsWith(".amazon.com")) return "amazon";
995
+ if (["baseAttributes", "discipline", "effortType", "targetRPE", "equipment", "aliases", "derivation"].some((key) => key in properties)) return "effort";
996
+ return "default";
997
+ }
998
+ function markdownSubtype(text) {
999
+ if (/^#{1,6}\s/.test(text)) return "heading";
1000
+ if (/^[-*+]\s|^\d+\.\s/.test(text)) return "list";
1001
+ if (/^>\s?/.test(text)) return "blockquote";
1002
+ if (text.startsWith("|") && /^\|[\s:]*-+/m.test(text)) return "table";
1003
+ return "paragraph";
1004
+ }
1005
+ function parseDocumentSections(content, options = {}) {
1006
+ if (!content) return [];
1007
+ const lines = content.split(/\r\n|\n|\r/);
1008
+ const offsets = [];
1009
+ const separators = [];
1010
+ let offset = 0;
1011
+ for (const text of lines) {
1012
+ offsets.push(offset);
1013
+ offset += text.length;
1014
+ const separator = content.startsWith("\r\n", offset) ? "\r\n" : /[\r\n]/.test(content[offset] ?? "") ? content[offset] : "";
1015
+ separators.push(separator);
1016
+ offset += separator.length;
1017
+ }
1018
+ const sections = [];
1019
+ const createdAt = Date.now();
1020
+ let firstText = true;
1021
+ let line = 0;
1022
+ function make(type, start, end) {
1023
+ const from = offsets[start];
1024
+ const to = offsets[end] + lines[end].length;
1025
+ return {
1026
+ id: crypto.randomUUID(),
1027
+ type,
1028
+ from,
1029
+ to,
1030
+ rawContent: content.slice(from, to),
1031
+ displayContent: content.slice(from, to),
1032
+ startLine: start,
1033
+ endLine: end,
1034
+ lineCount: end - start + 1,
1035
+ version: 1,
1036
+ createdAt,
1037
+ separatorAfter: separators[end]
1038
+ };
1039
+ }
1040
+ while (line < lines.length) {
1041
+ const text = lines[line].trim();
1042
+ const fence = text.match(/^(`{3,}|~{3,})(.*)$/);
1043
+ if (fence) {
1044
+ const marker = fence[1];
1045
+ let close = line + 1;
1046
+ const closing = new RegExp(`^${marker[0]}{${marker.length},}\\s*$`);
1047
+ while (close < lines.length && !closing.test(lines[close].trim())) close++;
1048
+ const closed = close < lines.length;
1049
+ const end2 = closed ? close : lines.length - 1;
1050
+ const tag = fence[2].trim();
1051
+ const dialect = marker[0] === "`" ? matchDialectFence(`\`\`\`${tag}`) : null;
1052
+ const widget = marker[0] === "`" ? tag.match(/^widget:([\w-]+)$/) : null;
1053
+ const query = marker[0] === "`" ? tag.match(/^query(?:[:\s](.*))?$/i) : null;
1054
+ const names = options.registeredWidgets ?? KNOWN_SECTION_WIDGETS;
1055
+ const knownWidget = widget && ("has" in names ? names.has(widget[1]) : names.includes(widget[1]));
1056
+ const type = closed && dialect ? dialect.dialect : closed && knownWidget ? "widget" : closed && query ? "query" : "code";
1057
+ const section2 = make(type, line, end2);
1058
+ section2.contentFrom = offsets[line] + lines[line].length + separators[line].length;
1059
+ section2.contentTo = closed ? offsets[close] : section2.to;
1060
+ section2.displayContent = content.slice(section2.contentFrom, section2.contentTo);
1061
+ if (dialect && closed) {
1062
+ section2.sport = dialect.sport;
1063
+ section2.contentId = blockContentId(section2.displayContent);
1064
+ section2.scriptBlock = {
1065
+ id: section2.id,
1066
+ contentId: section2.contentId,
1067
+ dialect: dialect.dialect,
1068
+ sport: dialect.sport,
1069
+ startLine: line,
1070
+ endLine: end2,
1071
+ content: section2.displayContent,
1007
1072
  state: "idle",
1008
1073
  version: 1,
1009
- createdAt: now
1074
+ createdAt
1010
1075
  };
1011
- blockContent = [];
1012
- return;
1013
1076
  }
1077
+ if (type === "widget" && widget) section2.widgetName = widget[1];
1078
+ if (type === "query" && query) section2.querySuffix = (query[1] ?? "").trim();
1079
+ sections.push(section2);
1080
+ line = end2 + 1;
1081
+ continue;
1014
1082
  }
1015
- if (inBlock && trimmedLine.startsWith("```")) {
1016
- inBlock = false;
1017
- currentBlock.endLine = index;
1018
- currentBlock.content = blockContent.join("\n");
1019
- blocks.push(currentBlock);
1020
- currentBlock = {};
1021
- blockContent = [];
1022
- } else if (inBlock) {
1023
- blockContent.push(line);
1083
+ if (text === "---" && lines[line + 1]?.trim()) {
1084
+ let close = line + 1;
1085
+ while (close < lines.length && lines[close].trim() !== "---" && !/^(`{3,}|~{3,})/.test(lines[close].trim())) close++;
1086
+ const inner = lines.slice(line + 1, close);
1087
+ if (lines[close]?.trim() === "---" && inner.some((value) => /^[A-Za-z_][\w-]*\s*:/.test(value.trim()))) {
1088
+ const section2 = make("frontmatter", line, close);
1089
+ section2.contentFrom = offsets[line + 1];
1090
+ section2.contentTo = offsets[close];
1091
+ section2.displayContent = content.slice(section2.contentFrom, section2.contentTo);
1092
+ section2.properties = {};
1093
+ for (const value of inner) {
1094
+ const pair = value.match(/^([^:]+):\s*(.*)$/);
1095
+ if (pair) section2.properties[pair[1].trim()] = pair[2].trim();
1096
+ }
1097
+ section2.frontmatterType = frontmatterSubtype(section2.properties);
1098
+ sections.push(section2);
1099
+ line = close + 1;
1100
+ continue;
1101
+ }
1024
1102
  }
1025
- });
1026
- if (inBlock && currentBlock.startLine !== void 0) {
1027
- currentBlock.endLine = lines.length - 1;
1028
- currentBlock.content = blockContent.join("\n");
1029
- blocks.push(currentBlock);
1030
- }
1031
- return blocks;
1032
- }
1033
- function findBlockAtLine(blocks, lineNumber) {
1034
- return blocks.find(
1035
- (block) => lineNumber >= block.startLine && lineNumber <= block.endLine
1036
- ) ?? null;
1037
- }
1038
- function extractBlockContent(content, startLine, endLine) {
1039
- const lines = content.split("\n");
1040
- return lines.slice(startLine + 1, endLine).join("\n");
1041
- }
1042
-
1043
- // src/sections/sectionParser.ts
1044
- var METADATA_REGEX = /<!--\s*section-metadata\s+id:(\S+)\s+version:(\d+)\s+created:(\d+)\s*-->/i;
1045
- function extractMetadata(text) {
1046
- const match = text.match(METADATA_REGEX);
1047
- if (!match) return { metadata: null, cleanText: text };
1048
- return {
1049
- metadata: {
1050
- id: match[1],
1051
- version: parseInt(match[2], 10),
1052
- createdAt: parseInt(match[3], 10)
1053
- },
1054
- cleanText: text.replace(METADATA_REGEX, "").trim()
1055
- };
1056
- }
1057
- function generateSectionId(type, startLine, content) {
1058
- let hash = 0;
1059
- for (let i = 0; i < content.length && i < 64; i++) {
1060
- hash = (hash << 5) - hash + content.charCodeAt(i) | 0;
1103
+ const embed = text.match(/^(!)?\[([^\]]*)\]\((https?:\/\/[^)\s]+|\/[^)\s]+)\)$/);
1104
+ if (embed) {
1105
+ const section2 = make("embed", line, line);
1106
+ const isImage = !!embed[1];
1107
+ section2.embed = { type: isImage ? "image" : /(?:youtube\.com|youtu\.be)/i.test(embed[3]) ? "youtube" : "link", label: embed[2], url: embed[3], isImage };
1108
+ sections.push(section2);
1109
+ line++;
1110
+ continue;
1111
+ }
1112
+ let end = line;
1113
+ if (text) {
1114
+ while (end + 1 < lines.length && lines[end + 1].trim() && !/^(?:`{3,}|~{3,}|---\s*$|!?\[[^\]]*\]\()/.test(lines[end + 1].trim())) end++;
1115
+ }
1116
+ const section = make(text && firstText ? "title" : "markdown", line, end);
1117
+ section.subtype = markdownSubtype(section.displayContent.trim());
1118
+ if (text) firstText = false;
1119
+ sections.push(section);
1120
+ line = end + 1;
1061
1121
  }
1062
- const hashHex = (hash >>> 0).toString(16).padStart(8, "0");
1063
- return `${type}-${startLine}-${hashHex}`;
1122
+ return sections;
1064
1123
  }
1065
- function detectUrlSubtype(url) {
1066
- if (!url) return null;
1067
- const withoutScheme = url.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
1068
- const host = withoutScheme.split(/[/?#:]/, 1)[0].toLowerCase();
1069
- if (host === "youtube.com" || host.endsWith(".youtube.com") || host === "youtu.be") return "youtube";
1070
- if (host === "strava.com" || host.endsWith(".strava.com")) return "strava";
1071
- if (host === "amazon.com" || host.endsWith(".amazon.com") || host === "amzn.to") return "amazon";
1124
+ function actionableSectionKind(section) {
1125
+ if (section.type === "time" || section.type === "log") return "wod";
1126
+ if (section.type === "query" || section.type === "widget" || section.type === "embed") return section.type;
1072
1127
  return null;
1073
1128
  }
1074
- function resolveFrontMatterSubtype(props) {
1075
- const typeValue = (props["type"] || "").toLowerCase();
1076
- if (typeValue === "youtube") return "youtube";
1077
- if (typeValue === "strava") return "strava";
1078
- if (typeValue === "amazon") return "amazon";
1079
- if (typeValue === "file") return "file";
1080
- if (typeValue === "effort") return "effort";
1081
- const url = props["url"] || props["link"] || "";
1082
- if (url) {
1083
- const detected = detectUrlSubtype(url);
1084
- if (detected) return detected;
1085
- }
1086
- if (props["baseAttributes"] !== void 0 || props["discipline"] !== void 0 || props["effortType"] !== void 0 || props["targetRPE"] !== void 0 || props["equipment"] !== void 0 || props["aliases"] !== void 0 || props["derivation"] !== void 0) {
1087
- return "effort";
1129
+ function reconcileNoteBlocks(rawContent, options) {
1130
+ if (!Number.isSafeInteger(options.sourceRevision) || options.sourceRevision < 1) throw new Error("Invalid source revision");
1131
+ const result = /* @__PURE__ */ Object.create(null);
1132
+ const lineage = Object.values(options.blocks ?? {}).filter((block) => block && typeof block === "object");
1133
+ const createId = options.createId ?? (() => crypto.randomUUID());
1134
+ for (const section of parseDocumentSections(rawContent, options)) {
1135
+ const kind = actionableSectionKind(section);
1136
+ if (!kind) continue;
1137
+ const matches = lineage.filter((block) => block.from === section.from && block.to === section.to && block.kind === kind);
1138
+ const prior = matches.length === 1 ? matches[0] : void 0;
1139
+ const id = prior?.id ?? createId();
1140
+ if (!id || Object.hasOwn(result, id)) throw new Error("Duplicate block occurrence ID");
1141
+ result[id] = {
1142
+ id,
1143
+ kind,
1144
+ from: section.from,
1145
+ to: section.to,
1146
+ sourceRevision: options.sourceRevision,
1147
+ version: prior?.version ?? 1,
1148
+ ...kind === "wod" && (section.type === "time" || section.type === "log") ? { dialect: section.type, ...section.sport ? { sport: section.sport } : {} } : {}
1149
+ };
1088
1150
  }
1089
- return "default";
1151
+ return result;
1090
1152
  }
1091
- function parseFrontMatterProperties(innerLines) {
1092
- const props = {};
1093
- for (const line of innerLines) {
1094
- const match = line.match(/^([^:]+):\s*(.*)$/);
1095
- if (match) {
1096
- props[match[1].trim()] = match[2].trim();
1097
- }
1153
+ function validateNoteBlocks(rawContent, blocks, sourceRevision, options = {}) {
1154
+ if (typeof rawContent !== "string" || !blocks || typeof blocks !== "object" || Array.isArray(blocks)) throw new Error("Invalid Note source or blocks");
1155
+ for (const [id, block] of Object.entries(blocks)) {
1156
+ if (!block || block.id !== id || !id || !Number.isSafeInteger(block.version) || block.version < 1 || block.sourceRevision !== sourceRevision || !Number.isSafeInteger(block.from) || !Number.isSafeInteger(block.to)) throw new Error("Invalid block identity, version or source revision");
1098
1157
  }
1099
- return props;
1100
- }
1101
- function matchMarkdownEmbed(trimmed) {
1102
- const match = trimmed.match(/^(!)?\[([^\]]*)\]\(([^)]+)\)$/);
1103
- if (!match) return null;
1104
- const isImage = !!match[1];
1105
- const label = match[2];
1106
- const url = match[3];
1107
- let type = isImage ? "image" : "link";
1108
- if (/youtube\.com|youtu\.be/i.test(url)) {
1109
- type = "youtube";
1110
- }
1111
- return { type, label, url, isImage };
1112
- }
1113
- function detectFrontMatterBlocks(lines, scriptBlocks) {
1114
- const ranges = [];
1115
- let i = 0;
1116
- while (i < lines.length) {
1117
- const inScript = scriptBlocks.some((b) => i >= b.startLine && i <= b.endLine);
1118
- if (inScript) {
1119
- i++;
1120
- continue;
1121
- }
1122
- if (lines[i].trim() === "---") {
1123
- const openLine = i;
1124
- let j = i + 1;
1125
- while (j < lines.length) {
1126
- const inScriptInner = scriptBlocks.some((b) => j >= b.startLine && j <= b.endLine);
1127
- if (inScriptInner) {
1128
- j++;
1129
- continue;
1130
- }
1131
- if (lines[j].trim() === "---") {
1132
- ranges.push({ startLine: openLine, endLine: j });
1133
- i = j + 1;
1134
- break;
1135
- }
1136
- j++;
1137
- }
1138
- if (j >= lines.length) {
1139
- i++;
1140
- }
1141
- } else {
1142
- i++;
1143
- }
1158
+ const expected = reconcileNoteBlocks(rawContent, { ...options, sourceRevision, blocks });
1159
+ if (Object.keys(expected).length !== Object.keys(blocks).length) throw new Error("Incomplete actionable block map");
1160
+ for (const [id, block] of Object.entries(blocks)) {
1161
+ const recognized = expected[id];
1162
+ if (!recognized || recognized.from !== block.from || recognized.to !== block.to || recognized.kind !== block.kind || recognized.dialect !== block.dialect || recognized.sport !== block.sport) throw new Error("Block does not match authored source");
1144
1163
  }
1145
- return ranges;
1146
1164
  }
1147
- function parseDocumentSections(content, scriptBlocks) {
1148
- if (!content) return [];
1149
- const blocks = scriptBlocks ?? detectScriptBlocks(content);
1150
- const lines = content.split("\n");
1151
- const sections = [];
1152
- const fmRanges = detectFrontMatterBlocks(lines, blocks);
1153
- const now = Date.now();
1154
- let currentLine = 0;
1155
- let isFirstTextBlock = true;
1156
- function flushMarkdownLines(mdLines, startLine) {
1157
- if (mdLines.length === 0) return;
1158
- let currentGroup = [];
1159
- let groupStartLine = startLine;
1160
- const flushGroup = () => {
1161
- if (currentGroup.length > 0) {
1162
- const raw = currentGroup.join("\n");
1163
- const { metadata, cleanText } = extractMetadata(raw);
1164
- const trimmed = cleanText.trim();
1165
- const embed = currentGroup.length === 1 ? matchMarkdownEmbed(trimmed) : null;
1166
- if (embed) {
1167
- sections.push({
1168
- id: metadata?.id || generateSectionId("embed", groupStartLine, trimmed),
1169
- type: "embed",
1170
- rawContent: cleanText,
1171
- displayContent: cleanText,
1172
- startLine: groupStartLine,
1173
- endLine: groupStartLine,
1174
- lineCount: 1,
1175
- embed,
1176
- version: metadata?.version || 1,
1177
- createdAt: metadata?.createdAt || now
1178
- });
1179
- } else if (isFirstTextBlock) {
1180
- isFirstTextBlock = false;
1181
- sections.push({
1182
- id: metadata?.id || generateSectionId("title", groupStartLine, cleanText),
1183
- type: "title",
1184
- rawContent: cleanText,
1185
- displayContent: cleanText,
1186
- startLine: groupStartLine,
1187
- endLine: groupStartLine + currentGroup.length - 1,
1188
- lineCount: currentGroup.length,
1189
- version: metadata?.version || 1,
1190
- createdAt: metadata?.createdAt || now
1191
- });
1192
- } else {
1193
- sections.push({
1194
- id: metadata?.id || generateSectionId("markdown", groupStartLine, cleanText),
1195
- type: "markdown",
1196
- rawContent: cleanText,
1197
- displayContent: cleanText,
1198
- startLine: groupStartLine,
1199
- endLine: groupStartLine + currentGroup.length - 1,
1200
- lineCount: currentGroup.length,
1201
- version: metadata?.version || 1,
1202
- createdAt: metadata?.createdAt || now
1203
- });
1204
- }
1205
- }
1206
- currentGroup = [];
1207
- };
1208
- for (let i = 0; i < mdLines.length; i++) {
1209
- const line = mdLines[i];
1210
- if (line.trim().length === 0) {
1211
- flushGroup();
1212
- sections.push({
1213
- id: generateSectionId("markdown", startLine + i, ""),
1214
- type: "markdown",
1215
- rawContent: "",
1216
- displayContent: "",
1217
- startLine: startLine + i,
1218
- endLine: startLine + i,
1219
- lineCount: 1,
1220
- version: 1,
1221
- createdAt: now
1222
- });
1223
- groupStartLine = startLine + i + 1;
1224
- } else {
1225
- currentGroup.push(line);
1165
+ function mapNoteBlockChanges(blocks, changes) {
1166
+ let previousEnd = 0;
1167
+ for (const change of changes) {
1168
+ if (!Number.isSafeInteger(change.from) || !Number.isSafeInteger(change.to) || change.from < previousEnd || change.to < change.from || typeof change.insert !== "string") throw new Error("Invalid source changes");
1169
+ previousEnd = change.to;
1170
+ }
1171
+ const result = {};
1172
+ for (const block of Object.values(blocks)) {
1173
+ let from = block.from;
1174
+ let to = block.to;
1175
+ let removed = false;
1176
+ for (const change of changes) {
1177
+ const delta = change.insert.length - (change.to - change.from);
1178
+ if (change.from <= block.from && change.to >= block.to || change.from < block.from && change.to > block.from || change.from < block.to && change.to > block.to) {
1179
+ removed = true;
1180
+ break;
1226
1181
  }
1182
+ if (change.to <= block.from) {
1183
+ from += delta;
1184
+ to += delta;
1185
+ } else if (change.from < block.to) to += delta;
1227
1186
  }
1228
- flushGroup();
1229
- }
1230
- let mdBuffer = [];
1231
- let mdBufferStart = 0;
1232
- while (currentLine < lines.length) {
1233
- const scriptBlock = blocks.find((b) => b.startLine === currentLine);
1234
- if (scriptBlock) {
1235
- flushMarkdownLines(mdBuffer, mdBufferStart);
1236
- mdBuffer = [];
1237
- const dialect = scriptBlock.dialect ?? "time";
1238
- const { metadata, cleanText } = extractMetadata(scriptBlock.content);
1239
- const fenceTag = scriptBlock.sport ? `${dialect}:${scriptBlock.sport}` : dialect;
1240
- const cleanRawContent = `\`\`\`${fenceTag}
1241
- ${cleanText}
1242
- \`\`\``;
1243
- const cleanLineCount = cleanRawContent.split("\n").length;
1244
- const sectionId = metadata?.id || generateSectionId(dialect, scriptBlock.startLine, cleanText);
1245
- const contentId = blockContentId(cleanText);
1246
- sections.push({
1247
- id: sectionId,
1248
- contentId,
1249
- type: dialect,
1250
- sport: scriptBlock.sport,
1251
- rawContent: cleanRawContent,
1252
- displayContent: cleanText,
1253
- startLine: scriptBlock.startLine,
1254
- endLine: scriptBlock.startLine + cleanLineCount - 1,
1255
- lineCount: cleanLineCount,
1256
- scriptBlock: {
1257
- ...scriptBlock,
1258
- id: sectionId,
1259
- contentId,
1260
- content: cleanText,
1261
- dialect,
1262
- version: metadata?.version || 1,
1263
- createdAt: metadata?.createdAt || now
1264
- },
1265
- version: metadata?.version || 1,
1266
- createdAt: metadata?.createdAt || now
1267
- });
1268
- currentLine = scriptBlock.endLine + 1;
1269
- mdBufferStart = currentLine;
1270
- continue;
1271
- }
1272
- const fmRange = fmRanges.find((r) => r.startLine === currentLine);
1273
- if (fmRange) {
1274
- flushMarkdownLines(mdBuffer, mdBufferStart);
1275
- mdBuffer = [];
1276
- const rawLines = lines.slice(fmRange.startLine, fmRange.endLine + 1);
1277
- const raw = rawLines.join("\n");
1278
- const innerLines = lines.slice(fmRange.startLine + 1, fmRange.endLine);
1279
- const { metadata, cleanText } = extractMetadata(innerLines.join("\n"));
1280
- const properties = parseFrontMatterProperties(cleanText.split("\n"));
1281
- const subtype = resolveFrontMatterSubtype(properties);
1282
- const lineCount = fmRange.endLine - fmRange.startLine + 1;
1283
- sections.push({
1284
- id: metadata?.id || generateSectionId("frontmatter", fmRange.startLine, raw),
1285
- type: "frontmatter",
1286
- rawContent: raw,
1287
- displayContent: cleanText,
1288
- startLine: fmRange.startLine,
1289
- endLine: fmRange.endLine,
1290
- lineCount,
1291
- properties,
1292
- frontmatterType: subtype,
1293
- version: metadata?.version || 1,
1294
- createdAt: metadata?.createdAt || now
1295
- });
1296
- currentLine = fmRange.endLine + 1;
1297
- mdBufferStart = currentLine;
1298
- continue;
1299
- }
1300
- if (mdBuffer.length === 0) {
1301
- mdBufferStart = currentLine;
1302
- }
1303
- mdBuffer.push(lines[currentLine]);
1304
- currentLine++;
1187
+ if (!removed) result[block.id] = { ...block, from, to };
1305
1188
  }
1306
- flushMarkdownLines(mdBuffer, mdBufferStart);
1307
- return sections;
1189
+ return result;
1308
1190
  }
1309
1191
  function buildRawContent(sections) {
1310
- return sections.filter((s) => !s.deleted).map((s) => s.rawContent).join("\n");
1192
+ return sections.filter((section) => !section.deleted).map((section) => section.rawContent + (section.separatorAfter ?? "")).join("");
1311
1193
  }
1312
1194
  function calculateTotalLines(sections) {
1313
- const visible = sections.filter((s) => !s.deleted);
1314
- if (visible.length === 0) return 0;
1315
- const last = visible[visible.length - 1];
1316
- return last.endLine + 1;
1195
+ const visible = sections.filter((section) => !section.deleted);
1196
+ return visible.length ? visible[visible.length - 1].endLine + 1 : 0;
1317
1197
  }
1318
- function matchSectionIds(oldSections, newSections) {
1319
- return newSections.map((newSec) => {
1320
- const { metadata } = extractMetadata(newSec.rawContent);
1321
- if (metadata) {
1322
- const updated = { ...newSec, id: metadata.id, version: metadata.version, createdAt: metadata.createdAt };
1323
- if (updated.scriptBlock) {
1324
- updated.scriptBlock = { ...updated.scriptBlock, id: metadata.id };
1325
- }
1326
- return updated;
1327
- }
1328
- const exactMatch = oldSections.find(
1329
- (old) => old.type === newSec.type && old.startLine === newSec.startLine && old.displayContent === newSec.displayContent
1330
- );
1331
- if (exactMatch) {
1332
- const updated = { ...newSec, id: exactMatch.id, version: exactMatch.version, createdAt: exactMatch.createdAt };
1333
- if (updated.scriptBlock) {
1334
- updated.scriptBlock = { ...updated.scriptBlock, id: exactMatch.id };
1335
- }
1336
- return updated;
1337
- }
1338
- const positionMatch = oldSections.find(
1339
- (old) => old.type === newSec.type && old.startLine === newSec.startLine
1340
- );
1341
- if (positionMatch) {
1342
- const newVersion = (positionMatch.version || 1) + 1;
1343
- const updated = {
1344
- ...newSec,
1345
- id: positionMatch.id,
1346
- version: newVersion,
1347
- createdAt: positionMatch.createdAt
1348
- };
1349
- if (updated.scriptBlock) {
1350
- updated.scriptBlock = { ...updated.scriptBlock, id: positionMatch.id, version: newVersion };
1351
- }
1352
- return updated;
1353
- }
1354
- return newSec;
1355
- });
1198
+
1199
+ // src/sections/blockDetection.ts
1200
+ function detectScriptBlocks(content) {
1201
+ return parseDocumentSections(content).flatMap((section) => section.scriptBlock ? [section.scriptBlock] : []);
1202
+ }
1203
+ function findBlockAtLine(blocks, lineNumber) {
1204
+ return blocks.find(
1205
+ (block) => lineNumber >= block.startLine && lineNumber <= block.endLine
1206
+ ) ?? null;
1207
+ }
1208
+ function extractBlockContent(content, startLine, endLine) {
1209
+ return parseDocumentSections(content).find((section) => section.startLine === startLine && section.endLine === endLine && section.scriptBlock)?.scriptBlock?.content ?? "";
1356
1210
  }
1357
1211
 
1358
1212
  // src/version.ts
1359
- var VERSION = "0.35.2116";
1360
- var GIT_SHA = "a4d411f";
1361
- var BUILD_TIME = "2026-10-12T00:14:24.795Z";
1213
+ var VERSION = "0.35.2117";
1214
+ var GIT_SHA = "03d1dfc";
1215
+ var BUILD_TIME = "2026-10-12T20:08:12.374Z";
1362
1216
  var SEMVER = {
1363
- version: "0.35.2116",
1364
- gitSha: "a4d411f",
1365
- buildTime: "2026-10-12T00:14:24.795Z"
1217
+ version: "0.35.2117",
1218
+ gitSha: "03d1dfc",
1219
+ buildTime: "2026-10-12T20:08:12.374Z"
1366
1220
  };
1367
1221
 
1368
- export { BUILD_TIME, BlockKey, CodeMetadata, CodeStatement, GIT_SHA, KNOWN_OUTPUT_TYPES, LEGACY_ORIGIN_TO_OWNERSHIP_LAYER, METRIC_OWNERSHIP_LAYER_CHAIN, Metric, MetricContainer, MetricType, NOTEBOOK_TAG_PREFIX, OutputStatement, OwnershipResolver, ParsedCodeStatement, SEMVER, SYSTEM_DEFAULT_OUTPUT_UNIT, UNIT_CATALOG, UnitConversionError, VALID_FENCE_DIALECTS, VERSION, blockContentId, buildRawContent, calculateTotalLines, convertViaCatalog, createMetricOwnershipLedger, detectScriptBlocks, dimensionKeyOf, extractBlockContent, fieldRefFromKey, fieldRefKey, findBlockAtLine, flattenFieldLeaves, fromNotebookTag, generateSectionId, getMetricOwnershipLayer, isNotebookTag, isWorkoutSectionType, lookupUnit, matchDialectFence, matchSectionIds, normalizeFieldComponent, normalizeFieldPath, ownershipRank, parseDocumentSections, resolveLayer, runAffordance, systemDefaultUnit, toNotebookTag, valueKindOf };
1222
+ export { BUILD_TIME, BlockKey, CodeMetadata, CodeStatement, GIT_SHA, KNOWN_OUTPUT_TYPES, KNOWN_SECTION_WIDGETS, LEGACY_ORIGIN_TO_OWNERSHIP_LAYER, METRIC_OWNERSHIP_LAYER_CHAIN, Metric, MetricContainer, MetricType, NOTEBOOK_TAG_PREFIX, OutputStatement, OwnershipResolver, ParsedCodeStatement, SEMVER, SYSTEM_DEFAULT_OUTPUT_UNIT, UNIT_CATALOG, UnitConversionError, VALID_FENCE_DIALECTS, VERSION, actionableSectionKind, blockContentId, buildRawContent, calculateTotalLines, convertViaCatalog, createMetricOwnershipLedger, detectScriptBlocks, dimensionKeyOf, extractBlockContent, fieldRefFromKey, fieldRefKey, findBlockAtLine, flattenFieldLeaves, fromNotebookTag, getMetricOwnershipLayer, isNotebookTag, isWorkoutSectionType, lookupUnit, mapNoteBlockChanges, matchDialectFence, normalizeFieldComponent, normalizeFieldPath, ownershipRank, parseDocumentSections, reconcileNoteBlocks, resolveLayer, runAffordance, systemDefaultUnit, toNotebookTag, validateNoteBlocks, valueKindOf };
1369
1223
  //# sourceMappingURL=index.mjs.map
1370
1224
  //# sourceMappingURL=index.mjs.map