Skip to content

Commit f534f65

Browse files
Copilotdanmarshall
andcommitted
Refactor: Add parseBody helper and update vega-lite and mermaid plugins
Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com>
1 parent 79e068c commit f534f65

3 files changed

Lines changed: 94 additions & 77 deletions

File tree

packages/markdown/src/plugins/config.ts

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,37 @@ export function parseFenceInfo(info: string): {
7979
return { format, pluginName, params, wasDefaultId };
8080
}
8181

82+
/**
83+
* Parse body content as JSON or YAML based on fence info.
84+
* @param content The fence content
85+
* @param info The fence info string (used to detect format)
86+
* @returns Parsed object and format metadata
87+
*/
88+
export function parseBody<T>(content: string, info: string): {
89+
spec: T | null;
90+
format: 'json' | 'yaml';
91+
error?: string;
92+
} {
93+
const { format } = parseFenceInfo(info);
94+
const formatName = format === 'yaml' ? 'YAML' : 'JSON';
95+
96+
try {
97+
let spec: T;
98+
if (format === 'yaml') {
99+
spec = yaml.load(content.trim()) as T;
100+
} else {
101+
spec = JSON.parse(content.trim());
102+
}
103+
return { spec, format };
104+
} catch (e) {
105+
return {
106+
spec: null,
107+
format,
108+
error: `malformed ${formatName}: ${e instanceof Error ? e.message : String(e)}`
109+
};
110+
}
111+
}
112+
82113
/*
83114
//Tests for parseFenceInfo
84115
const tests: [string, { format: 'json' | 'yaml'; pluginName: string; variableId: string | undefined; wasDefaultId: boolean }][] = [
@@ -135,45 +166,46 @@ tests.forEach(([input, expected], i) => {
135166
*/
136167

137168
/**
138-
* Creates a plugin that can parse both JSON and YAML formats
169+
* Creates a plugin that can parse both JSON and YAML formats.
170+
* This handles both "head" (fence info) and "body" (content) parsing.
139171
*/
140172
export function flaggablePlugin<T>(pluginName: PluginNames, className: string, flagger?: (spec: T) => RawFlaggableSpec<T>, attrs?: object) {
141173
const plugin: Plugin<T> = {
142174
name: pluginName,
143175
fence: (token, index) => {
144-
let content = token.content.trim();
145-
let spec: T;
146-
let flaggableSpec: RawFlaggableSpec<T>;
147-
148-
// Determine format from token info
149176
const info = token.info.trim();
150-
const isYaml = info.startsWith('yaml ');
151-
const formatName = isYaml ? 'YAML' : 'JSON';
177+
const content = token.content.trim();
152178

153-
try {
154-
if (isYaml) {
155-
spec = yaml.load(content) as T;
156-
} else {
157-
spec = JSON.parse(content);
158-
}
159-
} catch (e) {
179+
// Parse body content using the helper function
180+
const parseResult = parseBody<T>(content, info);
181+
182+
let flaggableSpec: RawFlaggableSpec<T>;
183+
if (parseResult.error) {
184+
// Parsing failed
160185
flaggableSpec = {
161186
spec: null,
162187
hasFlags: true,
163-
reasons: [`malformed ${formatName}`],
188+
reasons: [parseResult.error],
164189
};
165-
}
166-
if (spec) {
190+
} else if (parseResult.spec) {
191+
// Parsing succeeded, apply flagger if provided
167192
if (flagger) {
168-
flaggableSpec = flagger(spec);
193+
flaggableSpec = flagger(parseResult.spec);
169194
} else {
170-
flaggableSpec = { spec };
195+
flaggableSpec = { spec: parseResult.spec };
171196
}
197+
} else {
198+
// No spec (shouldn't happen, but handle it)
199+
flaggableSpec = {
200+
spec: null,
201+
hasFlags: true,
202+
reasons: ['No spec provided'],
203+
};
172204
}
173-
if (flaggableSpec) {
174-
content = JSON.stringify(flaggableSpec);
175-
}
176-
return sanitizedHTML('div', { class: className, id: `${pluginName}-${index}`, ...attrs }, content, true);
205+
206+
// Store the flaggable spec as JSON in the div
207+
const jsonContent = JSON.stringify(flaggableSpec);
208+
return sanitizedHTML('div', { class: className, id: `${pluginName}-${index}`, ...attrs }, jsonContent, true);
177209
},
178210
hydrateSpecs: (renderer, errorHandler) => {
179211
const flagged: SpecReview<T>[] = [];

packages/markdown/src/plugins/mermaid.ts

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,13 @@
4848
import { Plugin, RawFlaggableSpec, IInstance } from '../factory.js';
4949
import { ErrorHandler } from '../renderer.js';
5050
import { sanitizedHTML } from '../sanitize.js';
51-
import { flaggablePlugin } from './config.js';
51+
import { flaggablePlugin, parseBody } from './config.js';
5252
import { pluginClassName } from './util.js';
5353
import { PluginNames } from './interfaces.js';
5454
import { TemplateToken, tokenizeTemplate } from 'common';
5555
import { MermaidConfig } from 'mermaid';
5656
import type Mermaid from 'mermaid';
5757
import { MermaidElementProps, MermaidTemplate } from '@microsoft/chartifact-schema';
58-
import * as yaml from 'js-yaml';
5958

6059
interface MermaidInstance {
6160
id: string;
@@ -170,35 +169,23 @@ function loadMermaidFromCDN(): Promise<void> {
170169
export const mermaidPlugin: Plugin<MermaidSpec> = {
171170
...flaggablePlugin<MermaidSpec>(pluginName, className),
172171
fence: (token, index) => {
172+
const info = token.info.trim();
173173
const content = token.content.trim();
174+
175+
// Try to parse as JSON/YAML using the helper function
176+
const parseResult = parseBody<MermaidSpec>(content, info);
177+
174178
let spec: MermaidSpec;
175-
let flaggableSpec: RawFlaggableSpec<MermaidSpec>;
176-
177-
// Determine format from token info (like flaggablePlugin does)
178-
const info = token.info.trim();
179-
const isYaml = info.startsWith('yaml ');
180-
181-
// Try to parse as YAML or JSON based on format
182-
try {
183-
let parsed: any;
184-
if (isYaml) {
185-
parsed = yaml.load(content);
186-
} else {
187-
parsed = JSON.parse(content);
188-
}
189-
190-
if (parsed && typeof parsed === 'object') {
191-
spec = parsed as MermaidSpec;
192-
} else {
193-
// If it's valid YAML/JSON but not a proper MermaidSpec object, treat as raw text
194-
spec = { diagramText: content };
195-
}
196-
} catch (e) {
197-
// If YAML/JSON parsing fails, treat as raw text
179+
180+
if (parseResult.spec && typeof parseResult.spec === 'object') {
181+
// Parsing succeeded and it's an object - use it as MermaidSpec
182+
spec = parseResult.spec;
183+
} else {
184+
// If parsing failed or result is not an object, treat as raw text
198185
spec = { diagramText: content };
199186
}
200187

201-
flaggableSpec = inspectMermaidSpec(spec);
188+
const flaggableSpec = inspectMermaidSpec(spec);
202189
const json = JSON.stringify(flaggableSpec);
203190

204191
return sanitizedHTML('div', { class: className, id: `${pluginName}-${index}` }, json, true);

packages/markdown/src/plugins/vega-lite.ts

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,57 @@
55

66
import { Plugin, RawFlaggableSpec } from '../factory.js';
77
import { sanitizedHTML } from '../sanitize.js';
8-
import { flaggablePlugin } from './config.js';
8+
import { flaggablePlugin, parseBody } from './config.js';
99
import { pluginClassName } from './util.js';
1010
import { inspectVegaSpec, vegaPlugin } from './vega.js';
1111
import { compile, TopLevelSpec } from 'vega-lite';
1212
import { Spec } from 'vega';
1313
import { PluginNames } from './interfaces.js';
14-
import * as yaml from 'js-yaml';
1514

1615
const pluginName: PluginNames = 'vega-lite';
1716
const className = pluginClassName(pluginName);
1817

1918
export const vegaLitePlugin: Plugin<TopLevelSpec> = {
2019
...flaggablePlugin<TopLevelSpec>(pluginName, className),
2120
fence: (token, index) => {
22-
let content = token.content.trim();
23-
let spec: TopLevelSpec;
24-
let flaggableSpec: RawFlaggableSpec<Spec>;
25-
26-
// Determine format from token info
2721
const info = token.info.trim();
28-
const isYaml = info.startsWith('yaml ');
29-
const formatName = isYaml ? 'YAML' : 'JSON';
22+
const content = token.content.trim();
3023

31-
try {
32-
if (isYaml) {
33-
spec = yaml.load(content) as TopLevelSpec;
34-
} else {
35-
spec = JSON.parse(content);
36-
}
37-
} catch (e) {
24+
// Parse body content using the helper function
25+
const parseResult = parseBody<TopLevelSpec>(content, info);
26+
27+
let flaggableSpec: RawFlaggableSpec<Spec>;
28+
29+
if (parseResult.error) {
30+
// Parsing failed
3831
flaggableSpec = {
3932
spec: null,
4033
hasFlags: true,
41-
reasons: [`malformed ${formatName}`],
34+
reasons: [parseResult.error],
4235
};
43-
}
44-
if (spec) {
36+
} else if (parseResult.spec) {
37+
// Parsing succeeded, try to compile to Vega
4538
try {
46-
const vegaSpec = compile(spec);
39+
const vegaSpec = compile(parseResult.spec);
4740
flaggableSpec = inspectVegaSpec(vegaSpec.spec);
48-
}
49-
catch (e) {
41+
} catch (e) {
5042
flaggableSpec = {
5143
spec: null,
5244
hasFlags: true,
53-
reasons: [`failed to compile vega spec`],
45+
reasons: [`failed to compile vega spec: ${e instanceof Error ? e.message : String(e)}`],
5446
};
5547
}
48+
} else {
49+
// No spec (shouldn't happen)
50+
flaggableSpec = {
51+
spec: null,
52+
hasFlags: true,
53+
reasons: ['No spec provided'],
54+
};
5655
}
57-
if (flaggableSpec) {
58-
content = JSON.stringify(flaggableSpec);
59-
}
60-
return sanitizedHTML('div', { class: pluginClassName(vegaPlugin.name), id: `${pluginName}-${index}` }, content, true);
56+
57+
const jsonContent = JSON.stringify(flaggableSpec);
58+
return sanitizedHTML('div', { class: pluginClassName(vegaPlugin.name), id: `${pluginName}-${index}` }, jsonContent, true);
6159
},
6260
hydratesBefore: vegaPlugin.name,
6361
};

0 commit comments

Comments
 (0)