Repository: go-gitea/gitea
File: web_src/js/markup/mermaid.ts (TypeScript)

Code before:
  } catch {
    console.error('invalid or unsupported mermaid init YAML config', frontmatter);
  }
  return null;
}

function parseJsonInitConfig(source: string): MermaidConfig | null {
  // https://mermaid.js.org/config/directives.html#declaring-directives
  // TODO: mermaid is quite hacky for the "JSON" pattern, it can have other root keys
  //       and it can even accept invalid JSON string like:
  //       %%{initialize: { 'logLevel': 'fatal', "theme":'dark', 'startOnLoad': true } }%%
  // To keep things simple, Gitea only supports "init" directive with valid JSON string at the moment.
  const jsonInitConfigRegex = /%%\{\s*init\s*:\s*(.*?)\}%%/s;
  const jsonInit = (jsonInitConfigRegex.exec(source) || [])[1];
  if (!jsonInit) return null;
  try {
    return JSON.parse(jsonInit);
  } catch {
    console.error('invalid or unsupported mermaid init JSON config', jsonInit);
  }
  return null;
}

function configContainsElk(config: MermaidConfig | null) {
  if (config?.layout === 'elk' || config?.layout?.startsWith('elk.')) return true;
  return config?.flowchart?.defaultRenderer === 'elk';
}

/** detect whether mermaid sources contain elk layout configuration */
export function sourcesContainElk(sources: Array<string>) {
  for (const source of sources) {
    if (isSourceTooLarge(source)) continue;

    const yamlConfig = parseYamlInitConfig(source);
    if (configContainsElk(yamlConfig)) return true;

    const jsonConfig = parseJsonInitConfig(source);
    if (configContainsElk(jsonConfig)) return true;
  }

  return false;
}

async function loadMermaid(sources: Array<string>) {
  const mermaidPromise = import(/* webpackChunkName: "mermaid" */'mermaid');
  const elkPromise = sourcesContainElk(sources) ?
    import(/* webpackChunkName: "mermaid-layout-elk" */'@mermaid-js/layout-elk') : null;

  const results = await Promise.all([mermaidPromise, elkPromise]);
  return {
    mermaid: results[0].default,

Review comment (suggestion):
This is a regression, there are numerous other chart types in the `MermaidConfig` type that contain a `defaultRenderer` property, that's why I was using `Object.values`:

```ts
flowchart?: FlowchartDiagramConfig;
sequence?: SequenceDiagramConfig;
gantt?: GanttDiagramConfig;
journey?: JourneyDiagramConfig;
timeline?: TimelineDiagramConfig;
class?: ClassDiagramConfig;
state?: StateDiagramConfig;
er?: ErDiagramConfig;
pie?: PieDiagramConfig;
quadrantChart?: QuadrantChartConfig;
xyChart?: XYChartConfig;
requirement?: RequirementDiagramConfig;
architecture?: ArchitectureDiagramConfig;
mindmap?: MindmapDiagramConfig;
kanban?: KanbanDiagramConfig;
gitGraph?: GitGraphDiagramConfig;
c4?: C4DiagramConfig;
sankey?: SankeyDiagramConfig;
packet?: PacketDiagramConfig;
block?: BlockDiagramConfig;
radar?: RadarDiagramConfig;
```

Code after:
  } catch {
    console.error('invalid or unsupported mermaid init YAML config', frontmatter);
  }
  return null;
}

function parseJsonInitConfig(source: string): MermaidConfig | null {
  // https://mermaid.js.org/config/directives.html#declaring-directives
  // Do as dirty as mermaid does: https://github.com/mermaid-js/mermaid/blob/develop/packages/mermaid/src/utils.ts
  // It can even accept invalid JSON string like:
  // %%{initialize: { 'logLevel': 'fatal', "theme":'dark', 'startOnLoad': true } }%%
  const jsonInitConfigRegex = /%%\{\s*(init|initialize)\s*:\s*(.*?)\}%%/s;
  const jsonInitText = (jsonInitConfigRegex.exec(source) || [])[2];
  if (!jsonInitText) return null;
  try {
    const processed = jsonInitText.trim().replace(/'/g, '"');
    return JSON.parse(processed);
  } catch {
    console.error('invalid or unsupported mermaid init JSON config', jsonInitText);
  }
  return null;
}

function configValueIsElk(layoutOrRenderer: string | undefined) {
  if (typeof layoutOrRenderer !== 'string') return false;
  return layoutOrRenderer === 'elk' || layoutOrRenderer.startsWith('elk.');
}

function configContainsElk(config: MermaidConfig | null) {
  if (!config) return false;
  // Check the layout from the following properties:
  // * config.layout
  // * config.{any-diagram-config}.defaultRenderer
  //   Although only a few diagram types like "flowchart" support "defaultRenderer",
  //   as long as there is no side effect, here do a general check for all properties of "config", for ease of maintenance
  return configValueIsElk(config.layout) || Object.values(config).some((value) => configValueIsElk(value?.defaultRenderer));
}

/** detect whether mermaid sources contain elk layout configuration */
export function sourcesContainElk(sources: Array<string>) {
  for (const source of sources) {
    if (isSourceTooLarge(source)) continue;

    const yamlConfig = parseYamlInitConfig(source);
    if (configContainsElk(yamlConfig)) return true;

    const jsonConfig = parseJsonInitConfig(source);
    if (configContainsElk(jsonConfig)) return true;
  }

  return false;