jackyzha0 / quartz

🌱 a fast, batteries-included static-site generator that transforms Markdown content into fully functional websites
https://quartz.jzhao.xyz
MIT License
6.96k stars 2.48k forks source link

Excalidraw support discussion #1187

Open iceprosurface opened 4 months ago

iceprosurface commented 4 months ago

I want to add excalidraw support for quartz.

I made a plugin for setup excalidraw.

https://github.com/iceprosurface/quartz-excalidraw-plugin

And add Integration to quartz

https://github.com/iceprosurface/quartz-blog/commit/44fc388605e4dd4b0aa9fe2034c266d6d86d54a0

Demo:

demo-link

Currently, the integration method feels very cumbersome. Is there a better approach to directly embed it within Quartz?

iceprosurface commented 4 months ago

Excalidraw is very large, with the source code being over 1MB. I don't want to directly inline it in postscript.js.

jackyzha0 commented 4 months ago

We can try lazily instantiating it with the data it needs? This is what we do with mermaid for example

iceprosurface commented 4 months ago

I understand. It might be a better approach to first build an Excalidraw specifically for quartz and publish it to npm. Using upkg to asynchronously fetch the script could be a better option. Additionally, you must export a plugin to handle transform and emits.

Since the default API provided by Excalidraw does not perfectly match the data format of the obsidian-excalidraw-plugin, and the UI also needs some customization, it’s a bit troublesome that Quartz does not seem to support using Preact in inline scripts.

I'll push an implementation on Sunday and give it a try.

tharushkadinujaya05 commented 1 month ago

Hey, can u tell me how to setup this? i mean ur plugin. i want to use that :)

iceprosurface commented 1 month ago

@tharushkadinujaya05

I might need some help (ideas) to implement it into Quartz. Additionally, I haven't had much time recently to develop the feature.

BTW, currently it`s very hard to use the plugin.

1. create excalidraw plugin

For example:

https://github.com/iceprosurface/quartz-blog/tree/v4/packages/quartz-excalidraw-plugin

2. emit excalidraw origin file

Add an emit to quartz plugin for output excalidraw file(very hack way, use name)

Cannot get correct raw data of markdown ( any data with %%{raw}%% will ignore), so we should parse the origin markdown.

For example, https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/emitters/assets.ts#L12

const assets = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
const excalidrawAssets = await glob('**/*.excalidraw.md', argv.directory, cfg.configuration.ignorePatterns)
return [...excalidrawAssets, ...assets]

3. add script & content

3.1 add component

import { FullSlug, resolveRelative } from "../util/path"
import excalidrawScript from '<path-to-excalidraw-script>'
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"

export const Exclidraw: QuartzComponent = ({ classString, fileData }: QuartzComponentProps) => {
  return <article class={classString + ' popover-hint'}>
    <div class="excalidraw" style="width: 100%;height: 500px" data-excalidraw={resolveRelative(`${fileData.filePath!}` as FullSlug, fileData.slug!) + ".md"}></div>
  </article>
}

Exclidraw.afterDOMLoaded = excalidrawScript

3.2 add page support

Under: quartz/components/pages/Content.tsx

import { Exclidraw } from './../Excalidraw';
const Content: QuartzComponent = (props: QuartzComponentProps) => {
  //....
  const isExcalidraw = fileData.frontmatter?.['excalidraw-plugin'] ?? false;
  if (isExcalidraw) {
    return <Exclidraw {...props} />
  }
  //....

3.3 add script

Add load script for SPA mode, if there is not a SPA, async import should more convenient.

declare global {
  interface Window {
    scriptPromiseMap: Map<string, Promise<void>>;
  }
}
if (!window.scriptPromiseMap) {
  window.scriptPromiseMap = new Map();
}

export function loadScript(url: string, preserve = true) {
  let resolve: (value: void) => void = () => { };
  let reject: (reason?: any) => void = () => { };
  const promise = new Promise<void>((_resolve, _reject) => {
    resolve = _resolve;
    reject = _reject;
  });
  if (!url) {
    reject?.(new Error('URL is required'));
    return promise
  }
  if (window.scriptPromiseMap.get(url) && preserve) {
    return window.scriptPromiseMap.get(url) || Promise.resolve();
  }
  const script = document.createElement('script');
  script.src = url;
  script.async = true;
  if (preserve) {
    script.setAttribute('spa-preserve', 'true');
  }
  script.onload = () => {
    resolve();
  };
  script.onerror = () => {
    reject(new Error(`Failed to load script: ${url}`));
  };
  document.head.appendChild(script);
  if (preserve) {
    window.scriptPromiseMap.set(url, promise);
  }
  return promise
}

Add Excalidraw init script.

const pluginPath = '<path-to-plugin>'

export async function initExcalidraw() {
  await new Promise<void>((resolve) => {
    setTimeout(() => {
      resolve();
    }, 100);
  })
  const hasExcalidraw = document.querySelector('[data-excalidraw]');
  if (!hasExcalidraw) {
    return;
  }
  // can use import('xxx'), 
  await loadScript(pluginPath, false);
  const elements = document.querySelectorAll('[data-excalidraw]');
  if (!elements || !elements.length) {
    return;
  }
  elements.forEach((element) => {
    loadExcalidraw(element as HTMLElement);
  });
}
import { initExcalidraw } from "./util";
document.addEventListener('nav', (event) => {
  initExcalidraw();
})

3.4 add popover preview support

under: https://github.com/jackyzha0/quartz/blob/v4/quartz/components/scripts/popover.inline.ts#L87

      const excalidraw = html.querySelector("[data-excalidraw]")
      elts.forEach((elt) => popoverInner.appendChild(elt))
      if (excalidraw) {
        initExcalidraw()
      }

3.5 add style

Custom by yourself, excalidraw css will load by itself, and use unpkg cdn

3.6 add transform for image preview

Under: https://github.com/iceprosurface/quartz-blog/blob/v4/quartz/plugins/transformers/ofm.ts#L224

When markdownPlugins step, we should turn img into excalidraw layout.

 // embeds with excalidraw
      plugins.push(() => {
        return (tree: Root, file) => {
          // excalidraw
          visit(tree, "element", (node, index, parent) => {
            const path = node.properties.src;
            if (node.tagName === "img" && typeof path === "string" && path.endsWith(".excalidraw.md")) {
              node.tagName = "div"
              node.properties = {
                class: "excalidraw",
                style: "width: 100%;height: 500px",
                "data-excalidraw": path,
              }
            }
          })
        }
      })

summary

It is too complex and not so stable.