add markdown support
This commit is contained in:
152
src/lib/configTypes.ts
Normal file
152
src/lib/configTypes.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Registry of config node types. Each type defines syntax highlighting,
|
||||
* insert-menu blocks, and how to render the content (after Nunjucks) for the renderer.
|
||||
* Add a new entry here and wire its language in ConfigNode to add a new config type.
|
||||
*/
|
||||
|
||||
export type ConfigTypeId = 'plantuml' | 'markdown'
|
||||
|
||||
/** A single insert option (label + snippet to insert at cursor). */
|
||||
export type InsertBlock = { label: string; snippet: string }
|
||||
|
||||
/** A group of insert options (e.g. "Diagram" with startuml, actor, etc.). */
|
||||
export type InsertBlockGroup = { label: string; items: InsertBlock[] }
|
||||
|
||||
export type InsertBlockOrGroup = InsertBlock | InsertBlockGroup
|
||||
|
||||
function isGroup(b: InsertBlockOrGroup): b is InsertBlockGroup {
|
||||
return 'items' in b && Array.isArray((b as InsertBlockGroup).items)
|
||||
}
|
||||
|
||||
export type ConfigType = {
|
||||
id: ConfigTypeId
|
||||
label: string
|
||||
/** CodeMirror language key; used to pick the extension in ConfigNode. */
|
||||
language: ConfigTypeId
|
||||
/** Options under Insert → [type-specific]. Can be flat blocks or groups. */
|
||||
insertBlocks: InsertBlockOrGroup[]
|
||||
/** Render resolved content (after Nunjucks) to HTML/SVG string for the renderer. */
|
||||
render: (content: string) => Promise<string>
|
||||
}
|
||||
|
||||
const KROKI_PLANTUML_SVG = '/api/kroki/plantuml/svg'
|
||||
|
||||
const PLANTUML_INSERT_BLOCKS: InsertBlockOrGroup[] = [
|
||||
{ label: 'Diagram start/end', snippet: '@startuml\n\n@enduml' },
|
||||
{ label: 'Actor', snippet: 'actor ' },
|
||||
{ label: 'Participant', snippet: 'participant "" as ' },
|
||||
{ label: 'Arrow', snippet: ' -> ' },
|
||||
{ label: 'Note', snippet: 'note right of ' },
|
||||
{
|
||||
label: 'Nunjucks',
|
||||
items: [
|
||||
{ label: 'Variable {{ }}', snippet: '{{ }}' },
|
||||
{ label: 'if / endif', snippet: '{% if %}\n \n{% endif %}' },
|
||||
{ label: 'for / endfor', snippet: '{% for in %}\n \n{% endfor %}' },
|
||||
{ label: 'set', snippet: '{% set = %}' },
|
||||
{ label: 'extends', snippet: '{% extends "" %}' },
|
||||
{ label: 'include', snippet: '{% include "" %}' },
|
||||
{ label: 'import', snippet: '{% import "" as %}' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const MARKDOWN_INSERT_BLOCKS: InsertBlockOrGroup[] = [
|
||||
{ label: 'Heading 1', snippet: '# ' },
|
||||
{ label: 'Heading 2', snippet: '## ' },
|
||||
{ label: 'Heading 3', snippet: '### ' },
|
||||
{ label: 'Bold', snippet: '****' },
|
||||
{ label: 'Italic', snippet: '**' },
|
||||
{ label: 'Code inline', snippet: '``' },
|
||||
{ label: 'Code block', snippet: '```\n\n```' },
|
||||
{ label: 'Link', snippet: '[]()' },
|
||||
{ label: 'Image', snippet: '![]()' },
|
||||
{ label: 'List item', snippet: '- ' },
|
||||
{ label: 'Blockquote', snippet: '> ' },
|
||||
{
|
||||
label: 'Nunjucks',
|
||||
items: [
|
||||
{ label: 'Variable {{ }}', snippet: '{{ }}' },
|
||||
{ label: 'if / endif', snippet: '{% if %}\n \n{% endif %}' },
|
||||
{ label: 'for / endfor', snippet: '{% for in %}\n \n{% endfor %}' },
|
||||
{ label: 'set', snippet: '{% set = %}' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
/** Markdown to HTML using dynamic import to avoid loading if only PlantUML is used. */
|
||||
async function renderMarkdown(content: string): Promise<string> {
|
||||
const { marked } = await import('marked')
|
||||
const html = typeof marked.parse === 'function' ? await marked.parse(content) : (marked as (s: string) => string)(content)
|
||||
return typeof html === 'string' ? html : String(html)
|
||||
}
|
||||
|
||||
export const CONFIG_TYPES: ConfigType[] = [
|
||||
{
|
||||
id: 'plantuml',
|
||||
label: 'Diagram',
|
||||
language: 'plantuml',
|
||||
insertBlocks: PLANTUML_INSERT_BLOCKS,
|
||||
render: async (content: string) => {
|
||||
const res = await fetch(KROKI_PLANTUML_SVG, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: content,
|
||||
})
|
||||
if (!res.ok) {
|
||||
const err = await res.text()
|
||||
throw new Error(res.status === 400 ? err || 'Invalid PlantUML' : `Kroki error: ${res.status}`)
|
||||
}
|
||||
return res.text()
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'markdown',
|
||||
label: 'Markdown',
|
||||
language: 'markdown',
|
||||
insertBlocks: MARKDOWN_INSERT_BLOCKS,
|
||||
render: renderMarkdown,
|
||||
},
|
||||
]
|
||||
|
||||
export const CONFIG_TYPE_IDS = CONFIG_TYPES.map((t) => t.id)
|
||||
export const DEFAULT_CONFIG_TYPE_ID: ConfigTypeId = 'plantuml'
|
||||
|
||||
export function getConfigType(id: ConfigTypeId): ConfigType {
|
||||
const t = CONFIG_TYPES.find((c) => c.id === id)
|
||||
if (!t) throw new Error(`Unknown config type: ${id}`)
|
||||
return t
|
||||
}
|
||||
|
||||
export function getDefaultContentForConfigType(configTypeId: ConfigTypeId): string {
|
||||
if (configTypeId === 'plantuml') return '@startuml\n\n@enduml\n'
|
||||
if (configTypeId === 'markdown') return ''
|
||||
return ''
|
||||
}
|
||||
|
||||
/** Flatten insert blocks for iteration: either a single block or a group's items. */
|
||||
export function* iterateInsertBlocks(blocks: InsertBlockOrGroup[]): Generator<InsertBlock> {
|
||||
for (const b of blocks) {
|
||||
if (isGroup(b)) {
|
||||
for (const item of b.items) yield item
|
||||
} else {
|
||||
yield b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { isGroup }
|
||||
|
||||
/** Content from config node data (backward compat: content ?? plantuml). */
|
||||
export function getConfigContent(data: Record<string, unknown> | undefined): string {
|
||||
if (!data) return ''
|
||||
const content = data.content ?? data.plantuml
|
||||
return typeof content === 'string' ? content : ''
|
||||
}
|
||||
|
||||
/** Config type id from data (default plantuml). */
|
||||
export function getConfigTypeId(data: Record<string, unknown> | undefined): ConfigTypeId {
|
||||
if (!data || data.configType == null) return 'plantuml'
|
||||
const id = data.configType
|
||||
return CONFIG_TYPE_IDS.includes(id as ConfigTypeId) ? (id as ConfigTypeId) : 'plantuml'
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export const DEFAULT_NODE_STYLE: Record<string, { width: number; height: number
|
||||
}
|
||||
|
||||
const DEFAULT_DATA: Record<string, any> = {
|
||||
config: { plantuml: '@startuml\n\n@enduml\n', title: '' },
|
||||
config: { configType: 'plantuml', content: '@startuml\n\n@enduml\n', title: '' },
|
||||
render: {},
|
||||
variable: { value: '', valueType: 'string' },
|
||||
function: {
|
||||
@@ -91,7 +91,7 @@ export function getDefaultDataForType(type: string, newId?: string): any {
|
||||
/** Data for Reset action: clears code completely for config/function; same as default for others */
|
||||
export function getResetDataForType(type: string, nodeId?: string): any {
|
||||
if (type === 'config') {
|
||||
return { plantuml: '@startuml\n\n@enduml\n', title: nodeId ? `${nodeId}` : '' }
|
||||
return { configType: 'plantuml', content: '@startuml\n\n@enduml\n', title: nodeId ? `${nodeId}` : '' }
|
||||
}
|
||||
if (type === 'function') {
|
||||
return { body: '' }
|
||||
|
||||
Reference in New Issue
Block a user