markform 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +451 -240
- package/dist/ai-sdk.d.mts +1 -1
- package/dist/ai-sdk.mjs +2 -2
- package/dist/{apply-g23rRn7p.mjs → apply-B2kt6C2z.mjs} +136 -32
- package/dist/bin.mjs +1 -1
- package/dist/{cli-Bqlm-WWw.mjs → cli-Dt_PlYi_.mjs} +519 -60
- package/dist/cli.mjs +1 -1
- package/dist/{coreTypes-__Cwxz5q.mjs → coreTypes-B1oI7qvV.mjs} +52 -4
- package/dist/{coreTypes-DCvD7feM.d.mts → coreTypes-JCPm418M.d.mts} +265 -9
- package/dist/index.d.mts +22 -10
- package/dist/index.mjs +5 -5
- package/dist/{session-DruaYPZ1.mjs → session-CzCh6JeY.mjs} +1 -1
- package/dist/{session-CgCNni0e.mjs → session-Dxqwt0RC.mjs} +3 -3
- package/dist/{shared-C9yW5FLZ.mjs → shared-CNqwaxUt.mjs} +1 -1
- package/dist/{shared-DQ6y3Ggc.mjs → shared-D3dNi-Gn.mjs} +1 -1
- package/dist/{src-BiuxbzF3.mjs → src-DFsC5wwy.mjs} +308 -59
- package/docs/markform-apis.md +30 -1
- package/docs/markform-reference.md +65 -6
- package/docs/markform-spec.md +3 -3
- package/examples/movie-research/{movie-research-deep.form.md → movie-deep-research.form.md} +17 -58
- package/examples/movie-research/movie-research-demo.form.md +25 -34
- package/examples/rejection-test/rejection-test-mock-filled.form.md +41 -0
- package/examples/rejection-test/rejection-test-mock-filled.report.md +15 -0
- package/examples/rejection-test/rejection-test-mock-filled.schema.json +59 -0
- package/examples/rejection-test/rejection-test-mock-filled.yml +13 -0
- package/examples/rejection-test/rejection-test.form.md +35 -0
- package/examples/rejection-test/rejection-test.session.yaml +534 -0
- package/examples/simple/simple-mock-filled.report.md +96 -0
- package/examples/simple/simple-mock-filled.schema.json +374 -0
- package/examples/simple/simple-mock-filled.yml +87 -0
- package/examples/simple/simple-skipped-filled.report.md +90 -0
- package/examples/simple/simple-skipped-filled.schema.json +374 -0
- package/examples/simple/simple-skipped-filled.yml +77 -0
- package/examples/simple/simple-with-skips.session.yaml +1969 -21
- package/examples/simple/simple.session.yaml +1982 -21
- package/package.json +1 -1
- package/examples/earnings-analysis/earnings-analysis.form.md +0 -159
- package/examples/earnings-analysis/earnings-analysis.raw.md +0 -801
- package/examples/earnings-analysis/earnings-analysis.valid.ts +0 -198
- package/examples/movie-research/movie-research-basic.form.md +0 -169
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
import { L as PatchSchema } from "./coreTypes-
|
|
3
|
-
import { A as
|
|
4
|
-
import {
|
|
5
|
-
import { n as serializeSession } from "./session-
|
|
6
|
-
import { a as formatPath, c as logError, d as logTiming, f as logVerbose, g as writeFile, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-
|
|
2
|
+
import { L as PatchSchema } from "./coreTypes-B1oI7qvV.mjs";
|
|
3
|
+
import { A as deriveReportPath, C as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN, D as REPORT_EXTENSION, E as MAX_FORMS_IN_MENU, F as WEB_SEARCH_CONFIG, I as formatSuggestedLlms, M as detectFileType, N as parseRolesFlag, O as USER_ROLE, P as SUGGESTED_LLMS, R as hasWebSearchSupport, S as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, _ as DEFAULT_MAX_ISSUES_PER_TURN, b as DEFAULT_PORT, d as serialize, f as serializeRawMarkdown, g as DEFAULT_FORMS_DIR, h as ALL_EXTENSIONS, i as inspect, j as deriveSchemaPath, k as deriveExportPath, m as AGENT_ROLE, n as getAllFields, p as serializeReportMarkdown, t as applyPatches, v as DEFAULT_MAX_PATCHES_PER_TURN, y as DEFAULT_MAX_TURNS, z as parseModelIdForDisplay } from "./apply-B2kt6C2z.mjs";
|
|
4
|
+
import { D as parseForm, E as formToJsonSchema, a as resolveHarnessConfig, c as getProviderNames, d as createLiveAgent, h as createHarness, i as runResearch, l as resolveModel, n as isResearchForm, o as fillForm, p as createMockAgent, s as getProviderInfo, t as VERSION, u as buildMockWireFormat } from "./src-DFsC5wwy.mjs";
|
|
5
|
+
import { n as serializeSession } from "./session-Dxqwt0RC.mjs";
|
|
6
|
+
import { a as formatPath, c as logError, d as logTiming, f as logVerbose, g as writeFile, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-D3dNi-Gn.mjs";
|
|
7
7
|
import YAML from "yaml";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import pc from "picocolors";
|
|
@@ -365,7 +365,8 @@ function formatMarkdown$3(content) {
|
|
|
365
365
|
return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
|
|
366
366
|
});
|
|
367
367
|
formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
|
|
368
|
-
|
|
368
|
+
const attrsPart = attrs.trim() ? ` ${pc.dim(attrs.trim())}` : "";
|
|
369
|
+
return `${pc.dim("{% ")}${pc.green(tag)}${attrsPart} ${pc.dim("%}")}`;
|
|
369
370
|
});
|
|
370
371
|
formatted.push(formattedLine);
|
|
371
372
|
}
|
|
@@ -824,6 +825,23 @@ function toStructuredValues(form) {
|
|
|
824
825
|
case "url_list":
|
|
825
826
|
exportValue = value.items;
|
|
826
827
|
break;
|
|
828
|
+
case "date":
|
|
829
|
+
exportValue = value.value ?? null;
|
|
830
|
+
break;
|
|
831
|
+
case "year":
|
|
832
|
+
exportValue = value.value ?? null;
|
|
833
|
+
break;
|
|
834
|
+
case "table":
|
|
835
|
+
exportValue = value.rows.map((row) => {
|
|
836
|
+
const rowObj = {};
|
|
837
|
+
for (const [colId, cellResp] of Object.entries(row)) rowObj[colId] = cellResp.value ?? null;
|
|
838
|
+
return rowObj;
|
|
839
|
+
});
|
|
840
|
+
break;
|
|
841
|
+
default: {
|
|
842
|
+
const _exhaustive = value;
|
|
843
|
+
throw new Error(`Unhandled field value kind: ${_exhaustive.kind}`);
|
|
844
|
+
}
|
|
827
845
|
}
|
|
828
846
|
result[fieldId] = {
|
|
829
847
|
state: "answered",
|
|
@@ -924,7 +942,13 @@ function formatFieldResponse(response, useColors) {
|
|
|
924
942
|
}
|
|
925
943
|
case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
926
944
|
case "url_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
|
|
927
|
-
|
|
945
|
+
case "date": return value.value ? green(value.value) : dim("(empty)");
|
|
946
|
+
case "year": return value.value !== null ? green(String(value.value)) : dim("(empty)");
|
|
947
|
+
case "table": return value.rows.length > 0 ? green(`(${value.rows.length} rows)`) : dim("(empty)");
|
|
948
|
+
default: {
|
|
949
|
+
const _exhaustive = value;
|
|
950
|
+
throw new Error(`Unhandled field value kind: ${_exhaustive.kind}`);
|
|
951
|
+
}
|
|
928
952
|
}
|
|
929
953
|
}
|
|
930
954
|
/**
|
|
@@ -996,15 +1020,9 @@ const EXAMPLE_DEFINITIONS = [
|
|
|
996
1020
|
type: "fill"
|
|
997
1021
|
},
|
|
998
1022
|
{
|
|
999
|
-
id: "movie-research
|
|
1000
|
-
filename: "movie-research
|
|
1001
|
-
path: "movie-research/movie-research
|
|
1002
|
-
type: "research"
|
|
1003
|
-
},
|
|
1004
|
-
{
|
|
1005
|
-
id: "movie-research-deep",
|
|
1006
|
-
filename: "movie-research-deep.form.md",
|
|
1007
|
-
path: "movie-research/movie-research-deep.form.md",
|
|
1023
|
+
id: "movie-deep-research",
|
|
1024
|
+
filename: "movie-deep-research.form.md",
|
|
1025
|
+
path: "movie-research/movie-deep-research.form.md",
|
|
1008
1026
|
type: "research"
|
|
1009
1027
|
},
|
|
1010
1028
|
{
|
|
@@ -1012,12 +1030,6 @@ const EXAMPLE_DEFINITIONS = [
|
|
|
1012
1030
|
filename: "startup-deep-research.form.md",
|
|
1013
1031
|
path: "startup-deep-research/startup-deep-research.form.md",
|
|
1014
1032
|
type: "research"
|
|
1015
|
-
},
|
|
1016
|
-
{
|
|
1017
|
-
id: "earnings-analysis",
|
|
1018
|
-
filename: "earnings-analysis.form.md",
|
|
1019
|
-
path: "earnings-analysis/earnings-analysis.form.md",
|
|
1020
|
-
type: "research"
|
|
1021
1033
|
}
|
|
1022
1034
|
];
|
|
1023
1035
|
/** Default example ID for menus (movie-research-demo, index 0) */
|
|
@@ -1041,7 +1053,7 @@ function getExamplesDir() {
|
|
|
1041
1053
|
}
|
|
1042
1054
|
/**
|
|
1043
1055
|
* Load the content of an example form.
|
|
1044
|
-
* @param exampleId - The example ID (e.g., 'simple', 'movie-research
|
|
1056
|
+
* @param exampleId - The example ID (e.g., 'simple', 'movie-deep-research')
|
|
1045
1057
|
* @returns The form content as a string
|
|
1046
1058
|
* @throws Error if the example is not found
|
|
1047
1059
|
*/
|
|
@@ -1063,7 +1075,7 @@ function getExampleById(id) {
|
|
|
1063
1075
|
}
|
|
1064
1076
|
/**
|
|
1065
1077
|
* Get the absolute path to an example's source file.
|
|
1066
|
-
* @param exampleId - The example ID (e.g., 'simple', 'movie-research
|
|
1078
|
+
* @param exampleId - The example ID (e.g., 'simple', 'movie-deep-research')
|
|
1067
1079
|
* @returns The absolute path to the example form file
|
|
1068
1080
|
* @throws Error if the example is not found
|
|
1069
1081
|
*/
|
|
@@ -1088,7 +1100,7 @@ function extractFrontmatter(content) {
|
|
|
1088
1100
|
}
|
|
1089
1101
|
/**
|
|
1090
1102
|
* Load metadata (title, description) from an example's YAML frontmatter.
|
|
1091
|
-
* @param exampleId - The example ID (e.g., 'simple', 'movie-research
|
|
1103
|
+
* @param exampleId - The example ID (e.g., 'simple', 'movie-deep-research')
|
|
1092
1104
|
* @returns Object with title and description from frontmatter
|
|
1093
1105
|
*/
|
|
1094
1106
|
function loadExampleMetadata(exampleId) {
|
|
@@ -1129,7 +1141,7 @@ function issueReasonToStatus(reason) {
|
|
|
1129
1141
|
case "validation_error": return "invalid";
|
|
1130
1142
|
case "checkbox_incomplete": return "incomplete";
|
|
1131
1143
|
case "min_items_not_met": return "too-few";
|
|
1132
|
-
case "
|
|
1144
|
+
case "optional_unanswered": return "unanswered";
|
|
1133
1145
|
default: return "issue";
|
|
1134
1146
|
}
|
|
1135
1147
|
}
|
|
@@ -1152,7 +1164,7 @@ function formatTurnIssues(issues, maxShow = 5) {
|
|
|
1152
1164
|
/**
|
|
1153
1165
|
* Format form info for menu label display.
|
|
1154
1166
|
* Format: "filename - Title [runMode]"
|
|
1155
|
-
* Example: "movie-research
|
|
1167
|
+
* Example: "movie-deep-research.form.md - Movie Deep Research [research]"
|
|
1156
1168
|
*/
|
|
1157
1169
|
function formatFormLabel(info) {
|
|
1158
1170
|
const titlePart = info.title ? ` - ${info.title}` : "";
|
|
@@ -1169,7 +1181,7 @@ function formatFormHint(info) {
|
|
|
1169
1181
|
/**
|
|
1170
1182
|
* Format form info for log line (e.g., after copying).
|
|
1171
1183
|
* Format: "filename - Title" (dimmed title)
|
|
1172
|
-
* Example: "✓ movie-research
|
|
1184
|
+
* Example: "✓ movie-deep-research.form.md - Movie Deep Research"
|
|
1173
1185
|
*/
|
|
1174
1186
|
function formatFormLogLine(info, prefix) {
|
|
1175
1187
|
const titlePart = info.title ? ` - ${info.title}` : "";
|
|
@@ -2559,8 +2571,8 @@ function registerExamplesCommand(program) {
|
|
|
2559
2571
|
}
|
|
2560
2572
|
await copySingleExample(options.name, formsDir, ctx.overwrite, ctx.quiet);
|
|
2561
2573
|
} else {
|
|
2562
|
-
const { copied } = await copyAllExamples(formsDir, ctx.overwrite, ctx.quiet);
|
|
2563
|
-
if (!ctx.quiet && copied > 0) {
|
|
2574
|
+
const { copied, skipped } = await copyAllExamples(formsDir, ctx.overwrite, ctx.quiet);
|
|
2575
|
+
if (!ctx.quiet && (copied > 0 || skipped > 0)) {
|
|
2564
2576
|
console.log("");
|
|
2565
2577
|
const wantToRun = await p.confirm({
|
|
2566
2578
|
message: "Do you want to try running a form?",
|
|
@@ -2833,6 +2845,7 @@ function registerFillCommand(program) {
|
|
|
2833
2845
|
let mockPath;
|
|
2834
2846
|
let agentProvider;
|
|
2835
2847
|
let agentModelName;
|
|
2848
|
+
let targetRole = AGENT_ROLE;
|
|
2836
2849
|
let currentSpinner = null;
|
|
2837
2850
|
if (options.mock) {
|
|
2838
2851
|
mockPath = resolve(options.mockSource);
|
|
@@ -2860,12 +2873,12 @@ function registerFillCommand(program) {
|
|
|
2860
2873
|
error: (msg) => currentSpinner?.error(msg),
|
|
2861
2874
|
getElapsedMs: () => currentSpinner?.getElapsedMs() ?? 0
|
|
2862
2875
|
}, ctx);
|
|
2863
|
-
|
|
2876
|
+
targetRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0] ?? AGENT_ROLE;
|
|
2864
2877
|
const liveAgent = createLiveAgent({
|
|
2865
2878
|
model,
|
|
2866
2879
|
provider,
|
|
2867
2880
|
systemPromptAddition: systemPrompt,
|
|
2868
|
-
targetRole
|
|
2881
|
+
targetRole,
|
|
2869
2882
|
enableWebSearch: true,
|
|
2870
2883
|
callbacks
|
|
2871
2884
|
});
|
|
@@ -2881,6 +2894,7 @@ function registerFillCommand(program) {
|
|
|
2881
2894
|
logVerbose(ctx, `Target roles: ${targetRoles.includes("*") ? "*" : targetRoles.join(", ")}`);
|
|
2882
2895
|
logVerbose(ctx, `Fill mode: ${fillMode}`);
|
|
2883
2896
|
let stepResult = harness.step();
|
|
2897
|
+
let previousRejections;
|
|
2884
2898
|
logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
|
|
2885
2899
|
while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
|
|
2886
2900
|
let spinner = null;
|
|
@@ -2895,7 +2909,7 @@ function registerFillCommand(program) {
|
|
|
2895
2909
|
}
|
|
2896
2910
|
let response;
|
|
2897
2911
|
try {
|
|
2898
|
-
response = await agent.
|
|
2912
|
+
response = await agent.fillFormTool(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
|
|
2899
2913
|
spinner?.stop();
|
|
2900
2914
|
currentSpinner = null;
|
|
2901
2915
|
} catch (error) {
|
|
@@ -2927,12 +2941,28 @@ function registerFillCommand(program) {
|
|
|
2927
2941
|
}
|
|
2928
2942
|
}
|
|
2929
2943
|
let llmStats;
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2944
|
+
let context;
|
|
2945
|
+
let wire;
|
|
2946
|
+
if (stats) {
|
|
2947
|
+
llmStats = {
|
|
2948
|
+
inputTokens: stats.inputTokens,
|
|
2949
|
+
outputTokens: stats.outputTokens,
|
|
2950
|
+
toolCalls: stats.toolCalls.length > 0 ? stats.toolCalls : void 0
|
|
2951
|
+
};
|
|
2952
|
+
if (stats.prompts) context = {
|
|
2953
|
+
systemPrompt: stats.prompts.system,
|
|
2954
|
+
contextPrompt: stats.prompts.context
|
|
2955
|
+
};
|
|
2956
|
+
wire = stats.wire;
|
|
2957
|
+
} else if (options.mock) {
|
|
2958
|
+
wire = buildMockWireFormat(harness.getForm(), stepResult.issues, patches, harnessConfig.maxPatchesPerTurn, targetRole, previousRejections);
|
|
2959
|
+
context = {
|
|
2960
|
+
systemPrompt: wire.request.system,
|
|
2961
|
+
contextPrompt: wire.request.prompt
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
stepResult = harness.apply(patches, stepResult.issues, llmStats, context, wire);
|
|
2965
|
+
previousRejections = stepResult.rejectedPatches;
|
|
2936
2966
|
if (stepResult.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
|
|
2937
2967
|
else if (!harness.hasReachedMaxTurns()) {
|
|
2938
2968
|
stepResult = harness.step();
|
|
@@ -3073,7 +3103,13 @@ function formatFieldValue(value, useColors) {
|
|
|
3073
3103
|
}
|
|
3074
3104
|
case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
|
|
3075
3105
|
case "url_list": return value.items.length > 0 ? green(`[${value.items.map((i) => `"${i}"`).join(", ")}]`) : dim("(empty)");
|
|
3076
|
-
|
|
3106
|
+
case "date": return value.value ? green(value.value) : dim("(empty)");
|
|
3107
|
+
case "year": return value.value !== null ? green(String(value.value)) : dim("(empty)");
|
|
3108
|
+
case "table": return value.rows.length > 0 ? green(`(${value.rows.length} rows)`) : dim("(empty)");
|
|
3109
|
+
default: {
|
|
3110
|
+
const _exhaustive = value;
|
|
3111
|
+
throw new Error(`Unhandled field value kind: ${_exhaustive.kind}`);
|
|
3112
|
+
}
|
|
3077
3113
|
}
|
|
3078
3114
|
}
|
|
3079
3115
|
/**
|
|
@@ -3486,6 +3522,57 @@ function openBrowser(url) {
|
|
|
3486
3522
|
});
|
|
3487
3523
|
}
|
|
3488
3524
|
/**
|
|
3525
|
+
* Get the base path by stripping any known markform extension.
|
|
3526
|
+
*/
|
|
3527
|
+
function getBasePath(filePath) {
|
|
3528
|
+
for (const ext of Object.values(ALL_EXTENSIONS)) if (filePath.endsWith(ext)) return filePath.slice(0, -ext.length);
|
|
3529
|
+
if (filePath.endsWith(".md")) return filePath.slice(0, -3);
|
|
3530
|
+
return filePath;
|
|
3531
|
+
}
|
|
3532
|
+
/**
|
|
3533
|
+
* Find related files for a form file.
|
|
3534
|
+
* Checks for .report.md, .yml, and .schema.json files with the same base name.
|
|
3535
|
+
*/
|
|
3536
|
+
function findRelatedFiles(formPath) {
|
|
3537
|
+
const base = getBasePath(formPath);
|
|
3538
|
+
const reportPath = base + ALL_EXTENSIONS.report;
|
|
3539
|
+
const valuesPath = base + ALL_EXTENSIONS.yaml;
|
|
3540
|
+
const schemaPath = base + ALL_EXTENSIONS.schema;
|
|
3541
|
+
return {
|
|
3542
|
+
form: formPath,
|
|
3543
|
+
report: existsSync(reportPath) ? reportPath : null,
|
|
3544
|
+
values: existsSync(valuesPath) ? valuesPath : null,
|
|
3545
|
+
schema: existsSync(schemaPath) ? schemaPath : null
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
/**
|
|
3549
|
+
* Build tabs for tabbed navigation.
|
|
3550
|
+
*/
|
|
3551
|
+
function buildTabs(relatedFiles) {
|
|
3552
|
+
const tabs = [];
|
|
3553
|
+
tabs.push({
|
|
3554
|
+
id: "form",
|
|
3555
|
+
label: "Markform",
|
|
3556
|
+
path: relatedFiles.form
|
|
3557
|
+
});
|
|
3558
|
+
if (relatedFiles.report) tabs.push({
|
|
3559
|
+
id: "report",
|
|
3560
|
+
label: "Report",
|
|
3561
|
+
path: relatedFiles.report
|
|
3562
|
+
});
|
|
3563
|
+
if (relatedFiles.values) tabs.push({
|
|
3564
|
+
id: "values",
|
|
3565
|
+
label: "Values",
|
|
3566
|
+
path: relatedFiles.values
|
|
3567
|
+
});
|
|
3568
|
+
if (relatedFiles.schema) tabs.push({
|
|
3569
|
+
id: "schema",
|
|
3570
|
+
label: "Schema",
|
|
3571
|
+
path: relatedFiles.schema
|
|
3572
|
+
});
|
|
3573
|
+
return tabs;
|
|
3574
|
+
}
|
|
3575
|
+
/**
|
|
3489
3576
|
* Register the serve command.
|
|
3490
3577
|
*/
|
|
3491
3578
|
function registerServeCommand(program) {
|
|
@@ -3499,8 +3586,10 @@ function registerServeCommand(program) {
|
|
|
3499
3586
|
const content = await readFile$1(filePath);
|
|
3500
3587
|
let form = null;
|
|
3501
3588
|
if (fileType === "form") form = parseForm(content);
|
|
3589
|
+
const relatedFiles = fileType === "form" ? findRelatedFiles(filePath) : null;
|
|
3590
|
+
const tabs = relatedFiles ? buildTabs(relatedFiles) : null;
|
|
3502
3591
|
const server = createServer((req, res) => {
|
|
3503
|
-
handleRequest(req, res, filePath, fileType, form, ctx, (updatedForm) => {
|
|
3592
|
+
handleRequest(req, res, filePath, fileType, form, ctx, tabs, (updatedForm) => {
|
|
3504
3593
|
form = updatedForm;
|
|
3505
3594
|
}).catch((err) => {
|
|
3506
3595
|
console.error("Request error:", err);
|
|
@@ -3530,10 +3619,29 @@ function registerServeCommand(program) {
|
|
|
3530
3619
|
* Handle HTTP requests.
|
|
3531
3620
|
* Dispatches to appropriate renderer based on file type.
|
|
3532
3621
|
*/
|
|
3533
|
-
async function handleRequest(req, res, filePath, fileType, form, ctx, updateForm) {
|
|
3622
|
+
async function handleRequest(req, res, filePath, fileType, form, ctx, tabs, updateForm) {
|
|
3534
3623
|
const url = req.url ?? "/";
|
|
3624
|
+
if (req.method === "GET" && url.startsWith("/tab/") && tabs && tabs.length > 1) {
|
|
3625
|
+
const tabId = url.slice(5);
|
|
3626
|
+
const tab = tabs.find((t) => t.id === tabId);
|
|
3627
|
+
if (tab?.path) {
|
|
3628
|
+
const content = await readFile(tab.path, "utf-8");
|
|
3629
|
+
const tabFileType = detectFileType(tab.path);
|
|
3630
|
+
let html;
|
|
3631
|
+
if (tabFileType === "report" || tabFileType === "raw") html = renderMarkdownContent(content);
|
|
3632
|
+
else if (tabFileType === "yaml") html = renderYamlContent(content);
|
|
3633
|
+
else if (tabFileType === "json" || tabFileType === "schema") html = renderJsonContent(content);
|
|
3634
|
+
else html = `<pre>${escapeHtml(content)}</pre>`;
|
|
3635
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3636
|
+
res.end(html);
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
res.writeHead(404);
|
|
3640
|
+
res.end("Tab not found");
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
3535
3643
|
if (req.method === "GET" && url === "/") if (fileType === "form" && form) {
|
|
3536
|
-
const html = renderFormHtml(form);
|
|
3644
|
+
const html = renderFormHtml(form, tabs);
|
|
3537
3645
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3538
3646
|
res.end(html);
|
|
3539
3647
|
} else if (fileType === "raw" || fileType === "report") {
|
|
@@ -3544,7 +3652,7 @@ async function handleRequest(req, res, filePath, fileType, form, ctx, updateForm
|
|
|
3544
3652
|
const html = renderYamlHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
3545
3653
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3546
3654
|
res.end(html);
|
|
3547
|
-
} else if (fileType === "json") {
|
|
3655
|
+
} else if (fileType === "json" || fileType === "schema") {
|
|
3548
3656
|
const html = renderJsonHtml(await readFile(filePath, "utf-8"), basename(filePath));
|
|
3549
3657
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3550
3658
|
res.end(html);
|
|
@@ -3751,6 +3859,11 @@ function formDataToPatches(formData, form) {
|
|
|
3751
3859
|
});
|
|
3752
3860
|
break;
|
|
3753
3861
|
}
|
|
3862
|
+
case "table": break;
|
|
3863
|
+
default: {
|
|
3864
|
+
const _exhaustive = field;
|
|
3865
|
+
throw new Error(`Unhandled field kind: ${_exhaustive.kind}`);
|
|
3866
|
+
}
|
|
3754
3867
|
}
|
|
3755
3868
|
}
|
|
3756
3869
|
return patches;
|
|
@@ -3796,10 +3909,14 @@ async function handleSave(req, res, form, filePath, ctx, updateForm) {
|
|
|
3796
3909
|
* Render the form as HTML.
|
|
3797
3910
|
* @public Exported for testing.
|
|
3798
3911
|
*/
|
|
3799
|
-
function renderFormHtml(form) {
|
|
3912
|
+
function renderFormHtml(form, tabs) {
|
|
3800
3913
|
const { schema, responsesByFieldId } = form;
|
|
3801
3914
|
const formTitle = schema.title ?? schema.id;
|
|
3802
3915
|
const groupsHtml = schema.groups.map((group) => renderGroup(group, responsesByFieldId)).join("\n");
|
|
3916
|
+
const showTabs = tabs && tabs.length > 1;
|
|
3917
|
+
const tabBarHtml = showTabs ? `<div class="tab-bar">
|
|
3918
|
+
${tabs.map((tab, i) => `<button class="tab-btn${i === 0 ? " active" : ""}" data-tab="${tab.id}">${escapeHtml(tab.label)}</button>`).join("\n ")}
|
|
3919
|
+
</div>` : "";
|
|
3803
3920
|
return `<!DOCTYPE html>
|
|
3804
3921
|
<html lang="en">
|
|
3805
3922
|
<head>
|
|
@@ -3817,7 +3934,74 @@ function renderFormHtml(form) {
|
|
|
3817
3934
|
background: #f8f9fa;
|
|
3818
3935
|
color: #212529;
|
|
3819
3936
|
}
|
|
3820
|
-
h1 { color: #495057; border-bottom:
|
|
3937
|
+
h1 { color: #495057; border-bottom: none; padding-bottom: 0.5rem; }
|
|
3938
|
+
/* Tab bar styles */
|
|
3939
|
+
.tab-bar {
|
|
3940
|
+
display: flex;
|
|
3941
|
+
gap: 0.25rem;
|
|
3942
|
+
margin-bottom: 1.5rem;
|
|
3943
|
+
border-bottom: 2px solid #dee2e6;
|
|
3944
|
+
padding-bottom: 0;
|
|
3945
|
+
}
|
|
3946
|
+
.tab-btn {
|
|
3947
|
+
padding: 0.5rem 1rem;
|
|
3948
|
+
border: none;
|
|
3949
|
+
background: transparent;
|
|
3950
|
+
color: #6c757d;
|
|
3951
|
+
font-size: 0.95rem;
|
|
3952
|
+
cursor: pointer;
|
|
3953
|
+
border-bottom: 2px solid transparent;
|
|
3954
|
+
margin-bottom: -2px;
|
|
3955
|
+
transition: all 0.15s;
|
|
3956
|
+
}
|
|
3957
|
+
.tab-btn:hover {
|
|
3958
|
+
color: #495057;
|
|
3959
|
+
}
|
|
3960
|
+
.tab-btn.active {
|
|
3961
|
+
color: #0d6efd;
|
|
3962
|
+
border-bottom-color: #0d6efd;
|
|
3963
|
+
font-weight: 500;
|
|
3964
|
+
}
|
|
3965
|
+
.tab-content {
|
|
3966
|
+
display: none;
|
|
3967
|
+
}
|
|
3968
|
+
.tab-content.active {
|
|
3969
|
+
display: block;
|
|
3970
|
+
}
|
|
3971
|
+
/* Light theme syntax highlighting for tab content */
|
|
3972
|
+
.tab-content pre {
|
|
3973
|
+
background: #f8f9fa;
|
|
3974
|
+
color: #24292e;
|
|
3975
|
+
padding: 1rem;
|
|
3976
|
+
border-radius: 6px;
|
|
3977
|
+
border: 1px solid #e1e4e8;
|
|
3978
|
+
overflow-x: auto;
|
|
3979
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
3980
|
+
font-size: 0.9rem;
|
|
3981
|
+
line-height: 1.5;
|
|
3982
|
+
}
|
|
3983
|
+
.syn-key { color: #005cc5; }
|
|
3984
|
+
.syn-string { color: #22863a; }
|
|
3985
|
+
.syn-number { color: #005cc5; }
|
|
3986
|
+
.syn-bool { color: #d73a49; }
|
|
3987
|
+
.syn-null { color: #d73a49; }
|
|
3988
|
+
.syn-comment { color: #6a737d; font-style: italic; }
|
|
3989
|
+
/* Markdown content styles */
|
|
3990
|
+
.markdown-content { padding: 0.5rem 0; }
|
|
3991
|
+
.markdown-content h2 { font-size: 1.4rem; color: #24292e; margin: 1.5rem 0 0.75rem; }
|
|
3992
|
+
.markdown-content h3 { font-size: 1.2rem; color: #24292e; margin: 1.25rem 0 0.5rem; }
|
|
3993
|
+
.markdown-content h4 { font-size: 1.1rem; color: #24292e; margin: 1rem 0 0.5rem; }
|
|
3994
|
+
.markdown-content h5 { font-size: 1rem; color: #24292e; margin: 0.75rem 0 0.5rem; }
|
|
3995
|
+
.markdown-content p { margin: 0.75rem 0; line-height: 1.6; }
|
|
3996
|
+
.markdown-content li { margin: 0.25rem 0; margin-left: 1.5rem; line-height: 1.6; }
|
|
3997
|
+
.markdown-content code { background: #f1f3f5; padding: 0.2em 0.4em; border-radius: 3px; font-size: 0.9em; }
|
|
3998
|
+
.markdown-content pre { background: #f8f9fa; padding: 1rem; border-radius: 6px; border: 1px solid #e1e4e8; overflow-x: auto; }
|
|
3999
|
+
.markdown-content pre code { background: none; padding: 0; }
|
|
4000
|
+
.markdown-content a { color: #0366d6; text-decoration: none; }
|
|
4001
|
+
.markdown-content a:hover { text-decoration: underline; }
|
|
4002
|
+
.markdown-content strong { font-weight: 600; }
|
|
4003
|
+
.loading { text-align: center; padding: 2rem; color: #6c757d; }
|
|
4004
|
+
.error { text-align: center; padding: 2rem; color: #dc3545; }
|
|
3821
4005
|
h2 { color: #6c757d; font-size: 1.25rem; }
|
|
3822
4006
|
.group {
|
|
3823
4007
|
background: white;
|
|
@@ -3964,16 +4148,42 @@ function renderFormHtml(form) {
|
|
|
3964
4148
|
border-radius: 3px;
|
|
3965
4149
|
margin-left: 0.5rem;
|
|
3966
4150
|
}
|
|
4151
|
+
.table-container {
|
|
4152
|
+
overflow-x: auto;
|
|
4153
|
+
}
|
|
4154
|
+
.data-table {
|
|
4155
|
+
width: 100%;
|
|
4156
|
+
border-collapse: collapse;
|
|
4157
|
+
font-size: 0.9rem;
|
|
4158
|
+
}
|
|
4159
|
+
.data-table th,
|
|
4160
|
+
.data-table td {
|
|
4161
|
+
padding: 0.5rem 0.75rem;
|
|
4162
|
+
text-align: left;
|
|
4163
|
+
border: 1px solid #dee2e6;
|
|
4164
|
+
}
|
|
4165
|
+
.data-table th {
|
|
4166
|
+
background: #f8f9fa;
|
|
4167
|
+
font-weight: 600;
|
|
4168
|
+
color: #495057;
|
|
4169
|
+
}
|
|
4170
|
+
.data-table tbody tr:hover {
|
|
4171
|
+
background: #f8f9fa;
|
|
4172
|
+
}
|
|
3967
4173
|
</style>
|
|
3968
4174
|
</head>
|
|
3969
4175
|
<body>
|
|
3970
4176
|
<h1>${escapeHtml(formTitle)}</h1>
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
<
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
4177
|
+
${tabBarHtml}
|
|
4178
|
+
<div id="tab-form" class="tab-content active">
|
|
4179
|
+
<form method="POST" action="/save" id="markform">
|
|
4180
|
+
${groupsHtml}
|
|
4181
|
+
<div class="toolbar">
|
|
4182
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
|
4183
|
+
</div>
|
|
4184
|
+
</form>
|
|
4185
|
+
</div>
|
|
4186
|
+
${showTabs ? "<div id=\"tab-other\" class=\"tab-content\"><div class=\"loading\">Loading...</div></div>" : ""}
|
|
3977
4187
|
<script>
|
|
3978
4188
|
// Track fields marked for skip
|
|
3979
4189
|
const skippedFields = new Set();
|
|
@@ -4039,6 +4249,50 @@ function renderFormHtml(form) {
|
|
|
4039
4249
|
alert('Save failed: ' + err.message);
|
|
4040
4250
|
}
|
|
4041
4251
|
});
|
|
4252
|
+
|
|
4253
|
+
// Tab switching logic
|
|
4254
|
+
const tabButtons = document.querySelectorAll('.tab-btn');
|
|
4255
|
+
const tabFormContent = document.getElementById('tab-form');
|
|
4256
|
+
const tabOtherContent = document.getElementById('tab-other');
|
|
4257
|
+
const tabCache = {};
|
|
4258
|
+
|
|
4259
|
+
tabButtons.forEach(btn => {
|
|
4260
|
+
btn.addEventListener('click', async () => {
|
|
4261
|
+
const tabId = btn.dataset.tab;
|
|
4262
|
+
|
|
4263
|
+
// Update active button
|
|
4264
|
+
tabButtons.forEach(b => b.classList.remove('active'));
|
|
4265
|
+
btn.classList.add('active');
|
|
4266
|
+
|
|
4267
|
+
if (tabId === 'form') {
|
|
4268
|
+
// Show form tab
|
|
4269
|
+
if (tabFormContent) tabFormContent.classList.add('active');
|
|
4270
|
+
if (tabOtherContent) tabOtherContent.classList.remove('active');
|
|
4271
|
+
} else {
|
|
4272
|
+
// Show other tab content
|
|
4273
|
+
if (tabFormContent) tabFormContent.classList.remove('active');
|
|
4274
|
+
if (tabOtherContent) {
|
|
4275
|
+
tabOtherContent.classList.add('active');
|
|
4276
|
+
|
|
4277
|
+
// Fetch content if not cached
|
|
4278
|
+
if (!tabCache[tabId]) {
|
|
4279
|
+
tabOtherContent.innerHTML = '<div class="loading">Loading...</div>';
|
|
4280
|
+
try {
|
|
4281
|
+
const response = await fetch('/tab/' + tabId);
|
|
4282
|
+
if (response.ok) {
|
|
4283
|
+
tabCache[tabId] = await response.text();
|
|
4284
|
+
} else {
|
|
4285
|
+
tabCache[tabId] = '<div class="error">Failed to load content</div>';
|
|
4286
|
+
}
|
|
4287
|
+
} catch (err) {
|
|
4288
|
+
tabCache[tabId] = '<div class="error">Failed to load content</div>';
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
tabOtherContent.innerHTML = tabCache[tabId];
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
});
|
|
4295
|
+
});
|
|
4042
4296
|
<\/script>
|
|
4043
4297
|
</body>
|
|
4044
4298
|
</html>`;
|
|
@@ -4101,7 +4355,13 @@ function renderFieldHtml(field, value, isSkipped) {
|
|
|
4101
4355
|
case "year":
|
|
4102
4356
|
inputHtml = renderYearInput(field, value, disabledAttr);
|
|
4103
4357
|
break;
|
|
4104
|
-
|
|
4358
|
+
case "table":
|
|
4359
|
+
inputHtml = renderTableInput(field, value, disabledAttr);
|
|
4360
|
+
break;
|
|
4361
|
+
default: {
|
|
4362
|
+
const _exhaustive = field;
|
|
4363
|
+
throw new Error(`Unhandled field kind: ${_exhaustive.kind}`);
|
|
4364
|
+
}
|
|
4105
4365
|
}
|
|
4106
4366
|
const skipButton = !field.required && !skipped ? `<div class="field-actions">
|
|
4107
4367
|
<button type="button" class="btn-skip" data-skip-field="${field.id}">Skip</button>
|
|
@@ -4266,6 +4526,34 @@ function renderCheckboxesInput(field, value, disabledAttr) {
|
|
|
4266
4526
|
</div>`;
|
|
4267
4527
|
}
|
|
4268
4528
|
/**
|
|
4529
|
+
* Render a table field as an HTML table.
|
|
4530
|
+
* Currently read-only display; editing requires more complex UI.
|
|
4531
|
+
*/
|
|
4532
|
+
function renderTableInput(field, value, _disabledAttr) {
|
|
4533
|
+
const rows = value?.rows ?? [];
|
|
4534
|
+
if (rows.length === 0) return "<div class=\"field-help\">(no data)</div>";
|
|
4535
|
+
return `<div class="table-container">
|
|
4536
|
+
<table class="data-table">
|
|
4537
|
+
<thead>
|
|
4538
|
+
<tr>${field.columns.map((col) => `<th>${escapeHtml(col.label)}</th>`).join("")}</tr>
|
|
4539
|
+
</thead>
|
|
4540
|
+
<tbody>
|
|
4541
|
+
${rows.map((row) => {
|
|
4542
|
+
return `<tr>${field.columns.map((col) => {
|
|
4543
|
+
const cell = row[col.id];
|
|
4544
|
+
let cellValue = "";
|
|
4545
|
+
if (cell?.state === "answered" && cell.value !== void 0 && cell.value !== null) cellValue = String(cell.value);
|
|
4546
|
+
else if (cell?.state === "skipped") cellValue = cell.reason ? `[skipped: ${cell.reason}]` : "[skipped]";
|
|
4547
|
+
else if (cell?.state === "aborted") cellValue = cell.reason ? `[aborted: ${cell.reason}]` : "[aborted]";
|
|
4548
|
+
return `<td>${escapeHtml(cellValue)}</td>`;
|
|
4549
|
+
}).join("")}</tr>`;
|
|
4550
|
+
}).join("\n ")}
|
|
4551
|
+
</tbody>
|
|
4552
|
+
</table>
|
|
4553
|
+
<div class="field-help">(table fields are currently read-only in the web UI)</div>
|
|
4554
|
+
</div>`;
|
|
4555
|
+
}
|
|
4556
|
+
/**
|
|
4269
4557
|
* Escape HTML special characters.
|
|
4270
4558
|
* @public Exported for testing.
|
|
4271
4559
|
*/
|
|
@@ -4292,15 +4580,23 @@ const READ_ONLY_STYLES = `
|
|
|
4292
4580
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
4293
4581
|
}
|
|
4294
4582
|
pre {
|
|
4295
|
-
background: #
|
|
4296
|
-
color: #
|
|
4583
|
+
background: #f8f9fa;
|
|
4584
|
+
color: #24292e;
|
|
4297
4585
|
padding: 1rem;
|
|
4298
4586
|
border-radius: 6px;
|
|
4587
|
+
border: 1px solid #e1e4e8;
|
|
4299
4588
|
overflow-x: auto;
|
|
4300
4589
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
4301
4590
|
font-size: 0.9rem;
|
|
4302
4591
|
line-height: 1.5;
|
|
4303
4592
|
}
|
|
4593
|
+
/* Light theme syntax highlighting */
|
|
4594
|
+
.syn-key { color: #005cc5; }
|
|
4595
|
+
.syn-string { color: #22863a; }
|
|
4596
|
+
.syn-number { color: #005cc5; }
|
|
4597
|
+
.syn-bool { color: #d73a49; }
|
|
4598
|
+
.syn-null { color: #d73a49; }
|
|
4599
|
+
.syn-comment { color: #6a737d; font-style: italic; }
|
|
4304
4600
|
.badge {
|
|
4305
4601
|
font-size: 0.75rem;
|
|
4306
4602
|
padding: 0.2rem 0.5rem;
|
|
@@ -4379,9 +4675,23 @@ function renderMarkdownHtml(content, filename) {
|
|
|
4379
4675
|
*/
|
|
4380
4676
|
function renderYamlHtml(content, filename) {
|
|
4381
4677
|
const highlighted = content.split("\n").map((line) => {
|
|
4678
|
+
if (line.trim().startsWith("#")) return `<span class="syn-comment">${escapeHtml(line)}</span>`;
|
|
4382
4679
|
const colonIndex = line.indexOf(":");
|
|
4383
|
-
if (colonIndex > 0 && !line.trim().startsWith("
|
|
4384
|
-
|
|
4680
|
+
if (colonIndex > 0 && !line.trim().startsWith("-")) {
|
|
4681
|
+
const key = escapeHtml(line.slice(0, colonIndex));
|
|
4682
|
+
const afterColon = line.slice(colonIndex + 1).trim();
|
|
4683
|
+
const colonAndSpace = escapeHtml(line.slice(colonIndex, colonIndex + 1));
|
|
4684
|
+
if (afterColon === "") return `<span class="syn-key">${key}</span>${colonAndSpace}`;
|
|
4685
|
+
const valueStart = line.indexOf(afterColon, colonIndex);
|
|
4686
|
+
return `<span class="syn-key">${key}</span>${escapeHtml(line.slice(colonIndex, valueStart))}${highlightYamlValue(afterColon)}`;
|
|
4687
|
+
}
|
|
4688
|
+
if (line.trim().startsWith("-")) {
|
|
4689
|
+
const dashIndex = line.indexOf("-");
|
|
4690
|
+
const beforeDash = escapeHtml(line.slice(0, dashIndex));
|
|
4691
|
+
const afterDash = line.slice(dashIndex + 1).trim();
|
|
4692
|
+
if (afterDash === "") return `${beforeDash}-`;
|
|
4693
|
+
return `${beforeDash}- ${highlightYamlValue(afterDash)}`;
|
|
4694
|
+
}
|
|
4385
4695
|
return escapeHtml(line);
|
|
4386
4696
|
}).join("\n");
|
|
4387
4697
|
return `<!DOCTYPE html>
|
|
@@ -4401,6 +4711,17 @@ function renderYamlHtml(content, filename) {
|
|
|
4401
4711
|
</html>`;
|
|
4402
4712
|
}
|
|
4403
4713
|
/**
|
|
4714
|
+
* Highlight a YAML value based on its type.
|
|
4715
|
+
*/
|
|
4716
|
+
function highlightYamlValue(value) {
|
|
4717
|
+
const trimmed = value.trim();
|
|
4718
|
+
if (trimmed === "true" || trimmed === "false") return `<span class="syn-bool">${escapeHtml(value)}</span>`;
|
|
4719
|
+
if (trimmed === "null" || trimmed === "~") return `<span class="syn-null">${escapeHtml(value)}</span>`;
|
|
4720
|
+
if (/^-?\d+\.?\d*$/.test(trimmed)) return `<span class="syn-number">${escapeHtml(value)}</span>`;
|
|
4721
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return `<span class="syn-string">${escapeHtml(value)}</span>`;
|
|
4722
|
+
return `<span class="syn-string">${escapeHtml(value)}</span>`;
|
|
4723
|
+
}
|
|
4724
|
+
/**
|
|
4404
4725
|
* Render JSON content with syntax highlighting and formatting.
|
|
4405
4726
|
*/
|
|
4406
4727
|
function renderJsonHtml(content, filename) {
|
|
@@ -4411,7 +4732,7 @@ function renderJsonHtml(content, filename) {
|
|
|
4411
4732
|
} catch {
|
|
4412
4733
|
formatted = content;
|
|
4413
4734
|
}
|
|
4414
|
-
const highlighted = formatted.replace(/"([^"]+)":/g, "<span
|
|
4735
|
+
const highlighted = formatted.replace(/"([^"]+)":/g, "<span class=\"syn-key\">\"$1\"</span>:").replace(/: "([^"]*)"/g, ": <span class=\"syn-string\">\"$1\"</span>").replace(/: (-?\d+\.?\d*)/g, ": <span class=\"syn-number\">$1</span>").replace(/: (true|false)/g, ": <span class=\"syn-bool\">$1</span>").replace(/: (null)/g, ": <span class=\"syn-null\">$1</span>");
|
|
4415
4736
|
return `<!DOCTYPE html>
|
|
4416
4737
|
<html lang="en">
|
|
4417
4738
|
<head>
|
|
@@ -4448,6 +4769,144 @@ function renderPlainTextHtml(content, filename) {
|
|
|
4448
4769
|
</body>
|
|
4449
4770
|
</html>`;
|
|
4450
4771
|
}
|
|
4772
|
+
/**
|
|
4773
|
+
* Render markdown content (content only, no page wrapper).
|
|
4774
|
+
* Used for tab content.
|
|
4775
|
+
* @public Exported for testing.
|
|
4776
|
+
*/
|
|
4777
|
+
function renderMarkdownContent(content) {
|
|
4778
|
+
const lines = content.split("\n");
|
|
4779
|
+
let html = "<div class=\"markdown-content\">";
|
|
4780
|
+
let inParagraph = false;
|
|
4781
|
+
let inCodeBlock = false;
|
|
4782
|
+
let codeBlockContent = "";
|
|
4783
|
+
for (const line of lines) {
|
|
4784
|
+
const trimmed = line.trim();
|
|
4785
|
+
if (trimmed.startsWith("```")) {
|
|
4786
|
+
if (inCodeBlock) {
|
|
4787
|
+
html += `<pre><code>${escapeHtml(codeBlockContent.trim())}</code></pre>`;
|
|
4788
|
+
codeBlockContent = "";
|
|
4789
|
+
inCodeBlock = false;
|
|
4790
|
+
} else {
|
|
4791
|
+
if (inParagraph) {
|
|
4792
|
+
html += "</p>";
|
|
4793
|
+
inParagraph = false;
|
|
4794
|
+
}
|
|
4795
|
+
inCodeBlock = true;
|
|
4796
|
+
}
|
|
4797
|
+
continue;
|
|
4798
|
+
}
|
|
4799
|
+
if (inCodeBlock) {
|
|
4800
|
+
codeBlockContent += line + "\n";
|
|
4801
|
+
continue;
|
|
4802
|
+
}
|
|
4803
|
+
if (trimmed.startsWith("# ")) {
|
|
4804
|
+
if (inParagraph) {
|
|
4805
|
+
html += "</p>";
|
|
4806
|
+
inParagraph = false;
|
|
4807
|
+
}
|
|
4808
|
+
html += `<h2>${escapeHtml(trimmed.slice(2))}</h2>`;
|
|
4809
|
+
} else if (trimmed.startsWith("## ")) {
|
|
4810
|
+
if (inParagraph) {
|
|
4811
|
+
html += "</p>";
|
|
4812
|
+
inParagraph = false;
|
|
4813
|
+
}
|
|
4814
|
+
html += `<h3>${escapeHtml(trimmed.slice(3))}</h3>`;
|
|
4815
|
+
} else if (trimmed.startsWith("### ")) {
|
|
4816
|
+
if (inParagraph) {
|
|
4817
|
+
html += "</p>";
|
|
4818
|
+
inParagraph = false;
|
|
4819
|
+
}
|
|
4820
|
+
html += `<h4>${escapeHtml(trimmed.slice(4))}</h4>`;
|
|
4821
|
+
} else if (trimmed.startsWith("#### ")) {
|
|
4822
|
+
if (inParagraph) {
|
|
4823
|
+
html += "</p>";
|
|
4824
|
+
inParagraph = false;
|
|
4825
|
+
}
|
|
4826
|
+
html += `<h5>${escapeHtml(trimmed.slice(5))}</h5>`;
|
|
4827
|
+
} else if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
|
|
4828
|
+
if (inParagraph) {
|
|
4829
|
+
html += "</p>";
|
|
4830
|
+
inParagraph = false;
|
|
4831
|
+
}
|
|
4832
|
+
html += `<li>${formatInlineMarkdown(trimmed.slice(2))}</li>`;
|
|
4833
|
+
} else if (/^\d+\.\s/.test(trimmed)) {
|
|
4834
|
+
if (inParagraph) {
|
|
4835
|
+
html += "</p>";
|
|
4836
|
+
inParagraph = false;
|
|
4837
|
+
}
|
|
4838
|
+
const text = trimmed.replace(/^\d+\.\s/, "");
|
|
4839
|
+
html += `<li>${formatInlineMarkdown(text)}</li>`;
|
|
4840
|
+
} else if (trimmed === "") {
|
|
4841
|
+
if (inParagraph) {
|
|
4842
|
+
html += "</p>";
|
|
4843
|
+
inParagraph = false;
|
|
4844
|
+
}
|
|
4845
|
+
} else {
|
|
4846
|
+
if (!inParagraph) {
|
|
4847
|
+
html += "<p>";
|
|
4848
|
+
inParagraph = true;
|
|
4849
|
+
} else html += "<br>";
|
|
4850
|
+
html += formatInlineMarkdown(trimmed);
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
if (inParagraph) html += "</p>";
|
|
4854
|
+
html += "</div>";
|
|
4855
|
+
return html;
|
|
4856
|
+
}
|
|
4857
|
+
/**
|
|
4858
|
+
* Format inline markdown (bold, italic, code, links).
|
|
4859
|
+
*/
|
|
4860
|
+
function formatInlineMarkdown(text) {
|
|
4861
|
+
let result = escapeHtml(text);
|
|
4862
|
+
result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
4863
|
+
result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
4864
|
+
result = result.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
4865
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, linkText, url) => `<a href="${url.replace(/&/g, "&")}" target="_blank">${linkText}</a>`);
|
|
4866
|
+
return result;
|
|
4867
|
+
}
|
|
4868
|
+
/**
|
|
4869
|
+
* Render YAML content (content only, no page wrapper).
|
|
4870
|
+
* Used for tab content.
|
|
4871
|
+
* @public Exported for testing.
|
|
4872
|
+
*/
|
|
4873
|
+
function renderYamlContent(content) {
|
|
4874
|
+
return `<pre>${content.split("\n").map((line) => {
|
|
4875
|
+
if (line.trim().startsWith("#")) return `<span class="syn-comment">${escapeHtml(line)}</span>`;
|
|
4876
|
+
const colonIndex = line.indexOf(":");
|
|
4877
|
+
if (colonIndex > 0 && !line.trim().startsWith("-")) {
|
|
4878
|
+
const key = escapeHtml(line.slice(0, colonIndex));
|
|
4879
|
+
const afterColon = line.slice(colonIndex + 1).trim();
|
|
4880
|
+
const colonAndSpace = escapeHtml(line.slice(colonIndex, colonIndex + 1));
|
|
4881
|
+
if (afterColon === "") return `<span class="syn-key">${key}</span>${colonAndSpace}`;
|
|
4882
|
+
const valueStart = line.indexOf(afterColon, colonIndex);
|
|
4883
|
+
return `<span class="syn-key">${key}</span>${escapeHtml(line.slice(colonIndex, valueStart))}${highlightYamlValue(afterColon)}`;
|
|
4884
|
+
}
|
|
4885
|
+
if (line.trim().startsWith("-")) {
|
|
4886
|
+
const dashIndex = line.indexOf("-");
|
|
4887
|
+
const beforeDash = escapeHtml(line.slice(0, dashIndex));
|
|
4888
|
+
const afterDash = line.slice(dashIndex + 1).trim();
|
|
4889
|
+
if (afterDash === "") return `${beforeDash}-`;
|
|
4890
|
+
return `${beforeDash}- ${highlightYamlValue(afterDash)}`;
|
|
4891
|
+
}
|
|
4892
|
+
return escapeHtml(line);
|
|
4893
|
+
}).join("\n")}</pre>`;
|
|
4894
|
+
}
|
|
4895
|
+
/**
|
|
4896
|
+
* Render JSON content (content only, no page wrapper).
|
|
4897
|
+
* Used for tab content.
|
|
4898
|
+
* @public Exported for testing.
|
|
4899
|
+
*/
|
|
4900
|
+
function renderJsonContent(content) {
|
|
4901
|
+
let formatted;
|
|
4902
|
+
try {
|
|
4903
|
+
const parsed = JSON.parse(content);
|
|
4904
|
+
formatted = JSON.stringify(parsed, null, 2);
|
|
4905
|
+
} catch {
|
|
4906
|
+
formatted = content;
|
|
4907
|
+
}
|
|
4908
|
+
return `<pre>${formatted.replace(/"([^"]+)":/g, "<span class=\"syn-key\">\"$1\"</span>:").replace(/: "([^"]*)"/g, ": <span class=\"syn-string\">\"$1\"</span>").replace(/: (-?\d+\.?\d*)/g, ": <span class=\"syn-number\">$1</span>").replace(/: (true|false)/g, ": <span class=\"syn-bool\">$1</span>").replace(/: (null)/g, ": <span class=\"syn-null\">$1</span>")}</pre>`;
|
|
4909
|
+
}
|
|
4451
4910
|
|
|
4452
4911
|
//#endregion
|
|
4453
4912
|
//#region src/cli/commands/render.ts
|
|
@@ -4640,9 +5099,9 @@ function registerResearchCommand(program) {
|
|
|
4640
5099
|
console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
|
|
4641
5100
|
console.log(` ${schemaPath} ${pc.dim("(JSON Schema)")}`);
|
|
4642
5101
|
if (options.transcript && result.transcript) {
|
|
4643
|
-
const { serializeSession: serializeSession$1 } = await import("./session-
|
|
5102
|
+
const { serializeSession: serializeSession$1 } = await import("./session-CzCh6JeY.mjs");
|
|
4644
5103
|
const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
|
|
4645
|
-
const { writeFile: writeFile$1 } = await import("./shared-
|
|
5104
|
+
const { writeFile: writeFile$1 } = await import("./shared-CNqwaxUt.mjs");
|
|
4646
5105
|
await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
|
|
4647
5106
|
logInfo(ctx, `Transcript: ${transcriptPath}`);
|
|
4648
5107
|
}
|