kitfly 0.2.3 → 0.2.4
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/CHANGELOG.md +23 -0
- package/README.md +13 -11
- package/VERSION +1 -1
- package/dist/_raw/content/reference/gantt-widget.md +468 -0
- package/dist/_raw/content/reference/plugins.md +157 -2
- package/dist/content/deployment/preflight.html +5 -6
- package/dist/content/deployment/recipes/aws-s3.html +5 -6
- package/dist/content/deployment/recipes/cloudflare-pages.html +5 -6
- package/dist/content/deployment/recipes/cloudflare-r2.html +5 -6
- package/dist/content/deployment/recipes/fly-io.html +5 -6
- package/dist/content/deployment/recipes/github-pages.html +5 -6
- package/dist/content/deployment/recipes/netlify.html +5 -6
- package/dist/content/deployment/recipes/vercel.html +5 -6
- package/dist/content/deployment/secrets-and-env-vars.html +5 -6
- package/dist/content/deployment.html +5 -6
- package/dist/content/guide/approaches.html +5 -6
- package/dist/content/guide/branding.html +5 -6
- package/dist/content/guide/data-driven-content.html +5 -6
- package/dist/content/guide/features.html +5 -6
- package/dist/content/guide/getting-started.html +5 -6
- package/dist/content/guide/kitfly-overview.html +5 -6
- package/dist/content/reference/configuration.html +5 -6
- package/dist/content/reference/design-catalog.html +5 -6
- package/dist/content/reference/environment-variables.html +5 -6
- package/dist/content/reference/gantt-widget.html +899 -0
- package/dist/content/reference/glossary.html +5 -6
- package/dist/content/reference/key-concepts.html +5 -6
- package/dist/content/reference/plugins.html +245 -9
- package/dist/content/reference/slides-authoring-guidelines.html +5 -6
- package/dist/content/reference/structure.html +5 -6
- package/dist/content/reference.html +5 -6
- package/dist/content/templates/crucible.html +5 -6
- package/dist/content/templates/handbook.html +5 -6
- package/dist/content/templates/minimal.html +5 -6
- package/dist/content/templates/overview.html +5 -6
- package/dist/content/templates/pipeline.html +5 -6
- package/dist/content/templates/productbook.html +5 -6
- package/dist/content/templates/runbook.html +5 -6
- package/dist/content/templates/servicebook.html +5 -6
- package/dist/content-index.json +10 -2
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +5 -6
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +5 -6
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +5 -6
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +5 -6
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +5 -6
- package/dist/docs/decisions/ADR-0006-data-driven-content.html +5 -6
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +5 -6
- package/dist/docs/decisions/DDR-0002-theme-system.html +5 -6
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +5 -6
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +5 -6
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +5 -6
- package/dist/docs/userguide/cli/build.html +5 -6
- package/dist/docs/userguide/cli/bundle.html +5 -6
- package/dist/docs/userguide/cli/dev.html +5 -6
- package/dist/docs/userguide/cli/init.html +5 -6
- package/dist/docs/userguide/cli/servers.html +5 -6
- package/dist/docs/userguide/cli/stop.html +5 -6
- package/dist/docs/userguide/cli/update.html +5 -6
- package/dist/docs/userguide/cli/version.html +5 -6
- package/dist/docs/userguide/cli.html +5 -6
- package/dist/docs/userguide/sharing.html +5 -6
- package/dist/index.html +5 -6
- package/dist/llms.txt +3 -3
- package/dist/provenance.json +4 -5
- package/dist/reports/license-inventory.csv +199 -0
- package/dist/schemas/plugin-registry.schema.html +5 -6
- package/dist/schemas/plugin-schemas-notes.html +5 -6
- package/dist/schemas/plugin.schema.html +5 -6
- package/dist/schemas/plugins.schema.html +5 -6
- package/dist/schemas/v0/common.schema.html +5 -6
- package/dist/schemas/v0/plugin-registry.schema.html +5 -6
- package/dist/schemas/v0/plugin.schema.html +5 -6
- package/dist/schemas/v0/plugins.schema.html +5 -6
- package/dist/schemas/v0/site.schema.html +5 -6
- package/dist/schemas/v0/theme.schema.html +5 -6
- package/dist/schemas.html +5 -6
- package/package.json +1 -1
- package/plugins-dist/planning-visuals.css +261 -0
- package/plugins-dist/planning-visuals.js +669 -0
- package/registry/plugins.yaml +15 -1
- package/scripts/build-all.ts +5 -0
- package/scripts/build.ts +73 -11
- package/scripts/bundle.ts +73 -10
- package/scripts/dev.ts +49 -5
- package/scripts/embed-docs.ts +119 -0
- package/src/__tests__/build.test.ts +124 -0
- package/src/__tests__/bundle.test.ts +61 -0
- package/src/__tests__/docs.test.ts +117 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/bad-month-format.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/marker-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/milestone-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/missing-tracks.md +5 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/track-reversed.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-basic.md +15 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-no-milestones.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/month-basic.md +16 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/no-milestones.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/week-basic.md +20 -0
- package/src/__tests__/planning-visuals-fence-contract.test.ts +28 -0
- package/src/__tests__/planning-visuals-runtime-regressions.bun.test.ts +68 -0
- package/src/__tests__/planning-visuals-runtime.bun.test.ts +192 -0
- package/src/__tests__/shared.test.ts +121 -0
- package/src/cli.ts +113 -18
- package/src/commands/docs.ts +71 -0
- package/src/generated/embedded-docs.ts +2384 -0
- package/src/server-registry.ts +50 -10
- package/src/shared.ts +449 -25
package/src/server-registry.ts
CHANGED
|
@@ -81,24 +81,64 @@ async function writeRegistry(registry: ServerRegistry): Promise<void> {
|
|
|
81
81
|
|
|
82
82
|
import { listeningPorts, processList, procGet } from "@3leaps/sysprims";
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Windows fallback: parse `netstat -ano` to find PID listening on a port.
|
|
86
|
+
* Returns null if not found or on error.
|
|
87
|
+
*/
|
|
88
|
+
function findPidOnPortWindows(port: number): number | null {
|
|
89
|
+
try {
|
|
90
|
+
const result = Bun.spawnSync(["netstat", "-ano", "-p", "TCP"], {
|
|
91
|
+
stdout: "pipe",
|
|
92
|
+
stderr: "pipe",
|
|
93
|
+
});
|
|
94
|
+
if (result.exitCode !== 0) return null;
|
|
95
|
+
const output = new TextDecoder().decode(result.stdout);
|
|
96
|
+
// Each LISTENING line looks like:
|
|
97
|
+
// TCP 0.0.0.0:3333 0.0.0.0:0 LISTENING 1234
|
|
98
|
+
// TCP [::]:3333 [::]:0 LISTENING 1234
|
|
99
|
+
const portStr = String(port);
|
|
100
|
+
for (const line of output.split(/\r?\n/)) {
|
|
101
|
+
if (!line.includes("LISTENING")) continue;
|
|
102
|
+
// Match the local address column containing :<port>
|
|
103
|
+
const m = line.match(/^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)/i);
|
|
104
|
+
if (m && m[1] === portStr) {
|
|
105
|
+
const pid = parseInt(m[2], 10);
|
|
106
|
+
return Number.isNaN(pid) ? null : pid;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
84
115
|
/**
|
|
85
116
|
* Find PID listening on a port.
|
|
86
117
|
* When multiple processes bind the same port (e.g., nohup shell + bun child),
|
|
87
118
|
* prefer the bun process over shell wrappers.
|
|
119
|
+
* Falls back to netstat on Windows when sysprims is unsupported.
|
|
88
120
|
*/
|
|
89
121
|
export function findPidOnPort(port: number): number | null {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
122
|
+
try {
|
|
123
|
+
const result = listeningPorts({ local_port: port });
|
|
124
|
+
if (result.bindings.length === 0) return null;
|
|
125
|
+
if (result.bindings.length === 1) return result.bindings[0].pid ?? null;
|
|
126
|
+
|
|
127
|
+
// Multiple bindings — prefer the bun process over shell wrapper
|
|
128
|
+
for (const binding of result.bindings) {
|
|
129
|
+
if (binding.process?.name.includes("bun")) {
|
|
130
|
+
return binding.pid ?? null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Fallback: last binding (child is usually listed after parent)
|
|
134
|
+
return result.bindings[result.bindings.length - 1].pid ?? null;
|
|
135
|
+
} catch {
|
|
136
|
+
// sysprims doesn't support port bindings on this platform (e.g. Windows)
|
|
137
|
+
if (process.platform === "win32") {
|
|
138
|
+
return findPidOnPortWindows(port);
|
|
98
139
|
}
|
|
140
|
+
return null;
|
|
99
141
|
}
|
|
100
|
-
// Fallback: last binding (child is usually listed after parent)
|
|
101
|
-
return result.bindings[result.bindings.length - 1].pid ?? null;
|
|
102
142
|
}
|
|
103
143
|
|
|
104
144
|
/**
|
package/src/shared.ts
CHANGED
|
@@ -1008,17 +1008,16 @@ const SLIDES_VISUALS_TYPES = new Set([
|
|
|
1008
1008
|
"staircase",
|
|
1009
1009
|
]);
|
|
1010
1010
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
{
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
> = {
|
|
1011
|
+
type VisualListRule =
|
|
1012
|
+
| { kind: "strings" }
|
|
1013
|
+
| { kind: "objects"; fields: string[]; optional?: string[] };
|
|
1014
|
+
type VisualRules = {
|
|
1015
|
+
required: string[];
|
|
1016
|
+
scalars: string[];
|
|
1017
|
+
lists: Record<string, VisualListRule>;
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
const SLIDES_VISUALS_RULES: Record<string, VisualRules> = {
|
|
1022
1021
|
kpi: {
|
|
1023
1022
|
required: ["label", "value"],
|
|
1024
1023
|
scalars: ["label", "value", "trend"],
|
|
@@ -1091,11 +1090,44 @@ const SLIDES_VISUALS_RULES: Record<
|
|
|
1091
1090
|
},
|
|
1092
1091
|
};
|
|
1093
1092
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1093
|
+
const PLANNING_VISUALS_TYPES = new Set(["gantt"]);
|
|
1094
|
+
|
|
1095
|
+
const PLANNING_VISUALS_RULES: Record<string, VisualRules> = {
|
|
1096
|
+
gantt: {
|
|
1097
|
+
required: ["time-unit", "time-start", "time-end", "tracks"],
|
|
1098
|
+
scalars: ["label", "time-unit", "time-start", "time-end", "max-depth", "max-tracks", "today"],
|
|
1099
|
+
lists: {
|
|
1100
|
+
tracks: {
|
|
1101
|
+
kind: "objects",
|
|
1102
|
+
fields: ["label", "depth", "start", "end"],
|
|
1103
|
+
optional: ["status"],
|
|
1104
|
+
},
|
|
1105
|
+
milestones: {
|
|
1106
|
+
kind: "objects",
|
|
1107
|
+
fields: ["label", "date"],
|
|
1108
|
+
optional: ["depth"],
|
|
1109
|
+
},
|
|
1110
|
+
markers: {
|
|
1111
|
+
kind: "objects",
|
|
1112
|
+
fields: ["label", "date"],
|
|
1113
|
+
optional: ["color"],
|
|
1114
|
+
},
|
|
1115
|
+
},
|
|
1116
|
+
},
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
function parseQuotedScalar(raw: string): string {
|
|
1120
|
+
const trimmed = raw.trim();
|
|
1121
|
+
const match = trimmed.match(/^"(.*)"$/) || trimmed.match(/^'(.*)'$/);
|
|
1122
|
+
return match ? match[1] : trimmed;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function validateVisualFences(
|
|
1126
|
+
markdown: string,
|
|
1127
|
+
visualTypes: Set<string>,
|
|
1128
|
+
visualRules: Record<string, VisualRules>,
|
|
1129
|
+
unknownTypePrefix: string,
|
|
1130
|
+
): SlidesVisualsFenceDiagnostic[] {
|
|
1099
1131
|
const diagnostics: SlidesVisualsFenceDiagnostic[] = [];
|
|
1100
1132
|
const lines = markdown.replaceAll("\r\n", "\n").split("\n");
|
|
1101
1133
|
|
|
@@ -1113,7 +1145,7 @@ export function validateSlidesVisualsFences(markdown: string): SlidesVisualsFenc
|
|
|
1113
1145
|
}
|
|
1114
1146
|
|
|
1115
1147
|
function finishFence(closeLine: number) {
|
|
1116
|
-
const rules =
|
|
1148
|
+
const rules = visualRules[visualType];
|
|
1117
1149
|
if (!rules) return;
|
|
1118
1150
|
|
|
1119
1151
|
for (const key of rules.required) {
|
|
@@ -1159,10 +1191,10 @@ export function validateSlidesVisualsFences(markdown: string): SlidesVisualsFenc
|
|
|
1159
1191
|
const m = raw.match(/^:::\s*([a-z0-9-]+)\s*$/i);
|
|
1160
1192
|
if (!m) continue;
|
|
1161
1193
|
const type = m[1].toLowerCase();
|
|
1162
|
-
if (!
|
|
1194
|
+
if (!visualTypes.has(type)) {
|
|
1163
1195
|
diagnostics.push({
|
|
1164
1196
|
line: i + 1,
|
|
1165
|
-
message:
|
|
1197
|
+
message: `${unknownTypePrefix}${type}`,
|
|
1166
1198
|
type,
|
|
1167
1199
|
});
|
|
1168
1200
|
continue;
|
|
@@ -1177,7 +1209,6 @@ export function validateSlidesVisualsFences(markdown: string): SlidesVisualsFenc
|
|
|
1177
1209
|
continue;
|
|
1178
1210
|
}
|
|
1179
1211
|
|
|
1180
|
-
// inside visual fence
|
|
1181
1212
|
if (trimmed === ":::" && !raw.startsWith(":::")) {
|
|
1182
1213
|
err(i + 1, "Closing ::: fence must start at column 0");
|
|
1183
1214
|
continue;
|
|
@@ -1197,13 +1228,12 @@ export function validateSlidesVisualsFences(markdown: string): SlidesVisualsFenc
|
|
|
1197
1228
|
}
|
|
1198
1229
|
|
|
1199
1230
|
if (/^\s/.test(raw)) {
|
|
1200
|
-
// list item or list continuation
|
|
1201
1231
|
if (!currentListKey) {
|
|
1202
1232
|
err(i + 1, "Indented content is only allowed inside a list");
|
|
1203
1233
|
continue;
|
|
1204
1234
|
}
|
|
1205
1235
|
|
|
1206
|
-
const listRule =
|
|
1236
|
+
const listRule = visualRules[visualType]?.lists[currentListKey];
|
|
1207
1237
|
const item = raw.match(/^ {2}-\s+(.+)$/);
|
|
1208
1238
|
if (item) {
|
|
1209
1239
|
listItems += 1;
|
|
@@ -1234,7 +1264,7 @@ export function validateSlidesVisualsFences(markdown: string): SlidesVisualsFenc
|
|
|
1234
1264
|
continue;
|
|
1235
1265
|
}
|
|
1236
1266
|
|
|
1237
|
-
const rules =
|
|
1267
|
+
const rules = visualRules[visualType];
|
|
1238
1268
|
if (!rules) continue;
|
|
1239
1269
|
|
|
1240
1270
|
const kv = raw.match(/^([a-z][a-z0-9-]*)\s*:\s*(.*)$/i);
|
|
@@ -1247,7 +1277,6 @@ export function validateSlidesVisualsFences(markdown: string): SlidesVisualsFenc
|
|
|
1247
1277
|
const value = kv[2];
|
|
1248
1278
|
|
|
1249
1279
|
if (value === "") {
|
|
1250
|
-
// list key
|
|
1251
1280
|
const listRule = rules.lists[key];
|
|
1252
1281
|
if (!listRule) {
|
|
1253
1282
|
err(i + 1, `Key '${key}' is not a supported list for ${visualType}`);
|
|
@@ -1259,7 +1288,6 @@ export function validateSlidesVisualsFences(markdown: string): SlidesVisualsFenc
|
|
|
1259
1288
|
continue;
|
|
1260
1289
|
}
|
|
1261
1290
|
|
|
1262
|
-
// scalar key
|
|
1263
1291
|
if (!rules.scalars.includes(key)) {
|
|
1264
1292
|
err(i + 1, `Key '${key}' is not a supported scalar for ${visualType}`);
|
|
1265
1293
|
continue;
|
|
@@ -1279,12 +1307,408 @@ export function validateSlidesVisualsFences(markdown: string): SlidesVisualsFenc
|
|
|
1279
1307
|
return diagnostics;
|
|
1280
1308
|
}
|
|
1281
1309
|
|
|
1310
|
+
/**
|
|
1311
|
+
* Validate slides-visuals `:::` blocks in a single markdown slide body.
|
|
1312
|
+
* This contract is intentionally strict so writers/devs don’t guess at edge cases.
|
|
1313
|
+
*/
|
|
1314
|
+
export function validateSlidesVisualsFences(markdown: string): SlidesVisualsFenceDiagnostic[] {
|
|
1315
|
+
return validateVisualFences(
|
|
1316
|
+
markdown,
|
|
1317
|
+
SLIDES_VISUALS_TYPES,
|
|
1318
|
+
SLIDES_VISUALS_RULES,
|
|
1319
|
+
"Unknown slides-visuals block type: ",
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
type ParsedPlanningBlock = {
|
|
1324
|
+
startLine: number;
|
|
1325
|
+
data: Record<string, unknown>;
|
|
1326
|
+
scalarLines: Record<string, number>;
|
|
1327
|
+
listLines: Record<string, number>;
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
function parsePlanningGanttBlocks(markdown: string): ParsedPlanningBlock[] {
|
|
1331
|
+
const blocks: ParsedPlanningBlock[] = [];
|
|
1332
|
+
const lines = markdown.replaceAll("\r\n", "\n").split("\n");
|
|
1333
|
+
let mdFence: FenceState | null = null;
|
|
1334
|
+
let current: ParsedPlanningBlock | null = null;
|
|
1335
|
+
let currentList: string | null = null;
|
|
1336
|
+
let currentObject: Record<string, unknown> | null = null;
|
|
1337
|
+
|
|
1338
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1339
|
+
const raw = lines[i];
|
|
1340
|
+
const trimmed = raw.trim();
|
|
1341
|
+
const lineNo = i + 1;
|
|
1342
|
+
|
|
1343
|
+
mdFence = updateFenceState(trimmed, mdFence);
|
|
1344
|
+
if (mdFence) continue;
|
|
1345
|
+
|
|
1346
|
+
if (!current) {
|
|
1347
|
+
const open = raw.match(/^:::\s*([a-z0-9-]+)\s*$/i);
|
|
1348
|
+
if (!open || open[1].toLowerCase() !== "gantt") continue;
|
|
1349
|
+
current = {
|
|
1350
|
+
startLine: lineNo,
|
|
1351
|
+
data: {},
|
|
1352
|
+
scalarLines: {},
|
|
1353
|
+
listLines: {},
|
|
1354
|
+
};
|
|
1355
|
+
currentList = null;
|
|
1356
|
+
currentObject = null;
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
if (raw.match(/^:::\s*$/)) {
|
|
1361
|
+
blocks.push(current);
|
|
1362
|
+
current = null;
|
|
1363
|
+
currentList = null;
|
|
1364
|
+
currentObject = null;
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const kv = raw.match(/^([a-z][a-z0-9-]*)\s*:\s*(.*)$/i);
|
|
1369
|
+
if (kv) {
|
|
1370
|
+
const key = kv[1].toLowerCase();
|
|
1371
|
+
const value = kv[2];
|
|
1372
|
+
if (value === "") {
|
|
1373
|
+
currentList = key;
|
|
1374
|
+
currentObject = null;
|
|
1375
|
+
current.listLines[key] = lineNo;
|
|
1376
|
+
if (!Array.isArray(current.data[key])) current.data[key] = [];
|
|
1377
|
+
} else {
|
|
1378
|
+
current.data[key] = parseQuotedScalar(value);
|
|
1379
|
+
current.scalarLines[key] = lineNo;
|
|
1380
|
+
currentList = null;
|
|
1381
|
+
currentObject = null;
|
|
1382
|
+
}
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const item = raw.match(/^ {2}-\s+(.+)$/);
|
|
1387
|
+
if (item && currentList) {
|
|
1388
|
+
const list: unknown[] = Array.isArray(current.data[currentList])
|
|
1389
|
+
? (current.data[currentList] as unknown[])
|
|
1390
|
+
: [];
|
|
1391
|
+
current.data[currentList] = list;
|
|
1392
|
+
const objKV = item[1].match(/^([a-z][a-z0-9-]*)\s*:\s*(.+)$/i);
|
|
1393
|
+
if (objKV) {
|
|
1394
|
+
currentObject = { [objKV[1].toLowerCase()]: parseQuotedScalar(objKV[2]) };
|
|
1395
|
+
list.push(currentObject);
|
|
1396
|
+
} else {
|
|
1397
|
+
currentObject = null;
|
|
1398
|
+
list.push(parseQuotedScalar(item[1]));
|
|
1399
|
+
}
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const cont = raw.match(/^ {4}([a-z][a-z0-9-]*)\s*:\s*(.+)$/i);
|
|
1404
|
+
if (cont && currentObject) {
|
|
1405
|
+
currentObject[cont[1].toLowerCase()] = parseQuotedScalar(cont[2]);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
return blocks;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function isoWeeksInYear(year: number): number {
|
|
1413
|
+
const dec28 = new Date(Date.UTC(year, 11, 28));
|
|
1414
|
+
return getIsoWeekInfo(Math.floor(dec28.getTime() / (24 * 60 * 60 * 1000))).week;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function isoWeekMondayUtcMs(year: number, week: number): number {
|
|
1418
|
+
const jan4 = new Date(Date.UTC(year, 0, 4));
|
|
1419
|
+
const jan4Weekday = (jan4.getUTCDay() + 6) % 7; // Monday=0
|
|
1420
|
+
const weekOneMondayMs = jan4.getTime() - jan4Weekday * 24 * 60 * 60 * 1000;
|
|
1421
|
+
return weekOneMondayMs + (week - 1) * 7 * 24 * 60 * 60 * 1000;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function parseWeekOrdinal(value: string): number | null {
|
|
1425
|
+
const isoWeekAnchorDay = -3; // 1969-12-29 (Monday of 1970-W01)
|
|
1426
|
+
const match = value.match(/^(\d{4})-W(\d{2})$/i);
|
|
1427
|
+
if (!match) return null;
|
|
1428
|
+
const year = Number.parseInt(match[1], 10);
|
|
1429
|
+
const week = Number.parseInt(match[2], 10);
|
|
1430
|
+
if (week < 1 || week > isoWeeksInYear(year)) return null;
|
|
1431
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
1432
|
+
const mondayDayOrdinal = Math.floor(isoWeekMondayUtcMs(year, week) / dayMs);
|
|
1433
|
+
return Math.floor((mondayDayOrdinal - isoWeekAnchorDay) / 7);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function parseMonthOrdinal(value: string): number | null {
|
|
1437
|
+
const match = value.match(/^(\d{4})-(\d{2})$/);
|
|
1438
|
+
if (!match) return null;
|
|
1439
|
+
const year = Number.parseInt(match[1], 10);
|
|
1440
|
+
const month = Number.parseInt(match[2], 10);
|
|
1441
|
+
if (month < 1 || month > 12) return null;
|
|
1442
|
+
return year * 12 + (month - 1);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function daysInMonthUtc(year: number, month: number): number {
|
|
1446
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function parsePlanningMonthMarkerPosition(value: string): number | null {
|
|
1450
|
+
const monthOrdinal = parseMonthOrdinal(value);
|
|
1451
|
+
if (monthOrdinal != null) return monthOrdinal + 0.5;
|
|
1452
|
+
|
|
1453
|
+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
1454
|
+
if (!match) return null;
|
|
1455
|
+
const year = Number.parseInt(match[1], 10);
|
|
1456
|
+
const month = Number.parseInt(match[2], 10);
|
|
1457
|
+
const day = Number.parseInt(match[3], 10);
|
|
1458
|
+
if (month < 1 || month > 12) return null;
|
|
1459
|
+
const dim = daysInMonthUtc(year, month);
|
|
1460
|
+
if (day < 1 || day > dim) return null;
|
|
1461
|
+
const ordinal = year * 12 + (month - 1);
|
|
1462
|
+
return ordinal + (day - 0.5) / dim;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function parsePlanningUnitOrdinal(value: unknown, unit: string): number | null {
|
|
1466
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
1467
|
+
if (unit === "week") return parseWeekOrdinal(value.trim());
|
|
1468
|
+
if (unit === "month") return parseMonthOrdinal(value.trim());
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function parsePlanningMarkerPosition(value: unknown, unit: string): number | null {
|
|
1473
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
1474
|
+
const raw = value.trim();
|
|
1475
|
+
if (unit === "week") {
|
|
1476
|
+
const ordinal = parseWeekOrdinal(raw);
|
|
1477
|
+
return ordinal == null ? null : ordinal + 0.5;
|
|
1478
|
+
}
|
|
1479
|
+
if (unit === "month") {
|
|
1480
|
+
return parsePlanningMonthMarkerPosition(raw);
|
|
1481
|
+
}
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function getIsoWeekInfo(dayOrdinal: number): { year: number; week: number } {
|
|
1486
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
1487
|
+
const date = new Date(dayOrdinal * dayMs);
|
|
1488
|
+
const day = (date.getUTCDay() + 6) % 7; // Monday=0
|
|
1489
|
+
const thursday = new Date(date.getTime() + (3 - day) * dayMs);
|
|
1490
|
+
const year = thursday.getUTCFullYear();
|
|
1491
|
+
const firstThursday = new Date(Date.UTC(year, 0, 4));
|
|
1492
|
+
const firstThursdayDay = (firstThursday.getUTCDay() + 6) % 7;
|
|
1493
|
+
const firstThursdayOrdinal = Math.floor(firstThursday.getTime() / dayMs) + (3 - firstThursdayDay);
|
|
1494
|
+
const week = Math.floor((Math.floor(thursday.getTime() / dayMs) - firstThursdayOrdinal) / 7) + 1;
|
|
1495
|
+
return { year, week };
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
export function validatePlanningVisualsFences(markdown: string): SlidesVisualsFenceDiagnostic[] {
|
|
1499
|
+
const diagnostics = validateVisualFences(
|
|
1500
|
+
markdown,
|
|
1501
|
+
PLANNING_VISUALS_TYPES,
|
|
1502
|
+
PLANNING_VISUALS_RULES,
|
|
1503
|
+
"Unknown planning-visuals block type: ",
|
|
1504
|
+
);
|
|
1505
|
+
const ganttBlocks = parsePlanningGanttBlocks(markdown);
|
|
1506
|
+
|
|
1507
|
+
for (const block of ganttBlocks) {
|
|
1508
|
+
const unitRaw = block.data["time-unit"];
|
|
1509
|
+
const unit = typeof unitRaw === "string" ? unitRaw.trim().toLowerCase() : "";
|
|
1510
|
+
const unitLine = block.scalarLines["time-unit"] ?? block.startLine;
|
|
1511
|
+
const startLine = block.scalarLines["time-start"] ?? block.startLine;
|
|
1512
|
+
const endLine = block.scalarLines["time-end"] ?? block.startLine;
|
|
1513
|
+
const tracksLine = block.listLines.tracks ?? block.startLine;
|
|
1514
|
+
const milestonesLine = block.listLines.milestones ?? block.startLine;
|
|
1515
|
+
|
|
1516
|
+
if (unit !== "week" && unit !== "month") {
|
|
1517
|
+
diagnostics.push({
|
|
1518
|
+
line: unitLine,
|
|
1519
|
+
message: "Invalid time-unit (expected 'week' or 'month')",
|
|
1520
|
+
type: "gantt",
|
|
1521
|
+
});
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
const axisStart = parsePlanningUnitOrdinal(block.data["time-start"], unit);
|
|
1526
|
+
const axisEnd = parsePlanningUnitOrdinal(block.data["time-end"], unit);
|
|
1527
|
+
if (axisStart == null) {
|
|
1528
|
+
diagnostics.push({
|
|
1529
|
+
line: startLine,
|
|
1530
|
+
message: `Invalid time-start format for ${unit}`,
|
|
1531
|
+
type: "gantt",
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
if (axisEnd == null) {
|
|
1535
|
+
diagnostics.push({
|
|
1536
|
+
line: endLine,
|
|
1537
|
+
message: `Invalid time-end format for ${unit}`,
|
|
1538
|
+
type: "gantt",
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
if (axisStart != null && axisEnd != null && axisStart >= axisEnd) {
|
|
1542
|
+
diagnostics.push({
|
|
1543
|
+
line: endLine,
|
|
1544
|
+
message: "time-start must be before time-end",
|
|
1545
|
+
type: "gantt",
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const todayRaw = block.data.today;
|
|
1550
|
+
if (todayRaw != null && parsePlanningUnitOrdinal(todayRaw, unit) == null) {
|
|
1551
|
+
diagnostics.push({
|
|
1552
|
+
line: block.scalarLines.today ?? block.startLine,
|
|
1553
|
+
message: `Invalid today format for ${unit}`,
|
|
1554
|
+
type: "gantt",
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const tracks = Array.isArray(block.data.tracks) ? block.data.tracks : [];
|
|
1559
|
+
for (const track of tracks) {
|
|
1560
|
+
if (!track || typeof track !== "object") {
|
|
1561
|
+
diagnostics.push({
|
|
1562
|
+
line: tracksLine,
|
|
1563
|
+
message: "Track items must be objects",
|
|
1564
|
+
type: "gantt",
|
|
1565
|
+
});
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
const start = parsePlanningUnitOrdinal((track as Record<string, unknown>).start, unit);
|
|
1569
|
+
const end = parsePlanningUnitOrdinal((track as Record<string, unknown>).end, unit);
|
|
1570
|
+
if (start == null || end == null) {
|
|
1571
|
+
diagnostics.push({
|
|
1572
|
+
line: tracksLine,
|
|
1573
|
+
message: `Track start/end must match ${unit} format`,
|
|
1574
|
+
type: "gantt",
|
|
1575
|
+
});
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
if (start > end) {
|
|
1579
|
+
diagnostics.push({
|
|
1580
|
+
line: tracksLine,
|
|
1581
|
+
message: "Track start must be before or equal to end",
|
|
1582
|
+
type: "gantt",
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const milestones = Array.isArray(block.data.milestones) ? block.data.milestones : [];
|
|
1588
|
+
for (const milestone of milestones) {
|
|
1589
|
+
if (!milestone || typeof milestone !== "object") {
|
|
1590
|
+
diagnostics.push({
|
|
1591
|
+
line: milestonesLine,
|
|
1592
|
+
message: "Milestone items must be objects",
|
|
1593
|
+
type: "gantt",
|
|
1594
|
+
});
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
const date = parsePlanningUnitOrdinal((milestone as Record<string, unknown>).date, unit);
|
|
1598
|
+
if (date == null) {
|
|
1599
|
+
diagnostics.push({
|
|
1600
|
+
line: milestonesLine,
|
|
1601
|
+
message: `Milestone date must match ${unit} format`,
|
|
1602
|
+
type: "gantt",
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const markersLine = block.listLines.markers ?? block.startLine;
|
|
1608
|
+
const markers = Array.isArray(block.data.markers) ? block.data.markers : [];
|
|
1609
|
+
for (const marker of markers) {
|
|
1610
|
+
if (!marker || typeof marker !== "object") {
|
|
1611
|
+
diagnostics.push({
|
|
1612
|
+
line: markersLine,
|
|
1613
|
+
message: "Marker items must be objects",
|
|
1614
|
+
type: "gantt",
|
|
1615
|
+
});
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
const date = parsePlanningMarkerPosition((marker as Record<string, unknown>).date, unit);
|
|
1619
|
+
if (date == null) {
|
|
1620
|
+
const expected =
|
|
1621
|
+
unit === "month" ? "month format (YYYY-MM or YYYY-MM-DD)" : "week format (YYYY-Www)";
|
|
1622
|
+
diagnostics.push({
|
|
1623
|
+
line: markersLine,
|
|
1624
|
+
message: `Marker date must match ${expected}`,
|
|
1625
|
+
type: "gantt",
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
return diagnostics;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
export function collectPlanningVisualsContainmentWarnings(
|
|
1635
|
+
markdown: string,
|
|
1636
|
+
): SlidesVisualsFenceDiagnostic[] {
|
|
1637
|
+
const warnings: SlidesVisualsFenceDiagnostic[] = [];
|
|
1638
|
+
const ganttBlocks = parsePlanningGanttBlocks(markdown);
|
|
1639
|
+
|
|
1640
|
+
for (const block of ganttBlocks) {
|
|
1641
|
+
const unitRaw = block.data["time-unit"];
|
|
1642
|
+
const unit = typeof unitRaw === "string" ? unitRaw.trim().toLowerCase() : "";
|
|
1643
|
+
if (unit !== "week" && unit !== "month") continue;
|
|
1644
|
+
|
|
1645
|
+
const axisStart = parsePlanningUnitOrdinal(block.data["time-start"], unit);
|
|
1646
|
+
const axisEnd = parsePlanningUnitOrdinal(block.data["time-end"], unit);
|
|
1647
|
+
if (axisStart == null || axisEnd == null || axisStart >= axisEnd) continue;
|
|
1648
|
+
|
|
1649
|
+
const tracksLine = block.listLines.tracks ?? block.startLine;
|
|
1650
|
+
const milestonesLine = block.listLines.milestones ?? block.startLine;
|
|
1651
|
+
|
|
1652
|
+
const tracks = Array.isArray(block.data.tracks) ? block.data.tracks : [];
|
|
1653
|
+
for (const track of tracks) {
|
|
1654
|
+
if (!track || typeof track !== "object") continue;
|
|
1655
|
+
const start = parsePlanningUnitOrdinal((track as Record<string, unknown>).start, unit);
|
|
1656
|
+
const end = parsePlanningUnitOrdinal((track as Record<string, unknown>).end, unit);
|
|
1657
|
+
if (start == null || end == null) continue;
|
|
1658
|
+
if (start < axisStart || end > axisEnd) {
|
|
1659
|
+
warnings.push({
|
|
1660
|
+
line: tracksLine,
|
|
1661
|
+
message: "Track range is outside axis and will be clipped",
|
|
1662
|
+
type: "gantt",
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
const milestones = Array.isArray(block.data.milestones) ? block.data.milestones : [];
|
|
1668
|
+
for (const milestone of milestones) {
|
|
1669
|
+
if (!milestone || typeof milestone !== "object") continue;
|
|
1670
|
+
const date = parsePlanningUnitOrdinal((milestone as Record<string, unknown>).date, unit);
|
|
1671
|
+
if (date == null) continue;
|
|
1672
|
+
if (date < axisStart || date > axisEnd) {
|
|
1673
|
+
warnings.push({
|
|
1674
|
+
line: milestonesLine,
|
|
1675
|
+
message: "Milestone date is outside axis and will not be rendered",
|
|
1676
|
+
type: "gantt",
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const markersLine = block.listLines.markers ?? block.startLine;
|
|
1682
|
+
const markers = Array.isArray(block.data.markers) ? block.data.markers : [];
|
|
1683
|
+
for (const marker of markers) {
|
|
1684
|
+
if (!marker || typeof marker !== "object") continue;
|
|
1685
|
+
const position = parsePlanningMarkerPosition((marker as Record<string, unknown>).date, unit);
|
|
1686
|
+
if (position == null) continue;
|
|
1687
|
+
if (position < axisStart || position > axisEnd + 1) {
|
|
1688
|
+
warnings.push({
|
|
1689
|
+
line: markersLine,
|
|
1690
|
+
message: "Marker date is outside axis and will not be rendered",
|
|
1691
|
+
type: "gantt",
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
return warnings;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1282
1700
|
export function filterUnknownSlidesVisualsTypeDiagnostics(
|
|
1283
1701
|
diagnostics: SlidesVisualsFenceDiagnostic[],
|
|
1284
1702
|
): SlidesVisualsFenceDiagnostic[] {
|
|
1285
1703
|
return diagnostics.filter((d) => !d.message.startsWith("Unknown slides-visuals block type:"));
|
|
1286
1704
|
}
|
|
1287
1705
|
|
|
1706
|
+
export function filterUnknownPlanningVisualsTypeDiagnostics(
|
|
1707
|
+
diagnostics: SlidesVisualsFenceDiagnostic[],
|
|
1708
|
+
): SlidesVisualsFenceDiagnostic[] {
|
|
1709
|
+
return diagnostics.filter((d) => !d.message.startsWith("Unknown planning-visuals block type:"));
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1288
1712
|
/**
|
|
1289
1713
|
* Split markdown content into slide chunks using explicit delimiter.
|
|
1290
1714
|
* Delimiter lines inside fenced code blocks are ignored.
|