kgar / ts-markdown

An extensible TypeScript markdown generator that takes JSON and creates a markdown document
https://kgar.github.io/ts-markdown/
MIT License
9 stars 4 forks source link

Nested custom entries that reuse existing entries have prefixing side effects #37

Open kgar opened 2 years ago

kgar commented 2 years ago

Given this Obsidian Callout custom entry:

export type ObsidianCalloutEntry = {
  callout: {
    type: string;
    title?: InlineTypes[] | InlineTypes;
    content: MarkdownEntry | MarkdownEntry[];
    initialState?: 'expanded' | 'collapsed';
  };
};

export const calloutRenderer: MarkdownRenderer = (
  entry: ObsidianCalloutEntry,
  options: RenderOptions
) => {
  let titleContent = getTitleContent(entry);

  let content = Array.isArray(entry.callout.content)
    ? entry.callout.content
    : [entry.callout.content];

  let initialState =
    entry.callout.initialState === 'collapsed'
      ? '-'
      : entry.callout.initialState === 'expanded'
      ? '+'
      : '';

  const blockquote: BlockquoteEntry = {
    blockquote: [
      { p: [`[!${entry.callout.type}]${initialState}`, ...titleContent] },
      ...content,
    ],
  };

  return {
    markdown: renderEntries([blockquote], { ...options),
    blockLevel: true,
  };
};

function getTitleContent(entry: ObsidianCalloutEntry): InlineTypes[] {
  if (!entry.callout.title) {
    return [];
  }

  const title = entry.callout.title;
  let normalizedTitle = Array.isArray(title) ? title : [title];

  return [' ', ...normalizedTitle];
}

The expected format of nested callouts is to append one additional layer of blockquote. So, a callout within a callout should look like this:

> [!info]+ Some Title Here
> 
> > [!quote]+ 
> > 
> > Here's a quote.

Instead, it looks like this:

> [!info]+ Some Title Here
> 
> > > [!quote]+ 
> > > 
> > > Here's a quote.

In general, this is because the callout implementation is calling renderEnties(), which is the established example in the docs.

The workaround is to update the callout renderer as follows:

  return {
    markdown: renderEntries([blockquote], { ...options, prefix: '' }),
    blockLevel: true,
  };

This works, but it feels awkward to do, and it's not obvious to an implementer of custom renderers which reuse existing renderers.

I'm not sure yet on a solution for this that will prevent headaches for custom entry authors, but I'm putting the issue here for now so it can be explored later.