Keisuke ANDO
2025-05-27 51b43a2115a6b38bbaad67b203c2158a387b5588
feat(links): added ofm option to style unresolved or broken links differently (#1992)

* feat: add option to disable broken wikilinks

* fix(style): update hover color for broken links and introduce new class

* feat: add "disableBrokenWikilinks" option to ObsidianFlavoredMarkdown
3 files modified
28 ■■■■■ changed files
docs/plugins/ObsidianFlavoredMarkdown.md 1 ●●●● patch | view | raw | blame | history
quartz/plugins/transformers/ofm.ts 16 ●●●●● patch | view | raw | blame | history
quartz/styles/base.scss 11 ●●●●● patch | view | raw | blame | history
docs/plugins/ObsidianFlavoredMarkdown.md
@@ -23,6 +23,7 @@
- `enableYouTubeEmbed`: If `true` (default), enables the embedding of YouTube videos and playlists using external image Markdown syntax.
- `enableVideoEmbed`: If `true` (default), enables the embedding of video files.
- `enableCheckbox`: If `true`, adds support for interactive checkboxes in content. Defaults to `false`.
- `disableBrokenWikilinks`: If `true`, replaces links to non-existent notes with a dimmed, disabled link. Defaults to `false`.
> [!warning]
> Don't remove this plugin if you're using [[Obsidian compatibility|Obsidian]] to author the content!
quartz/plugins/transformers/ofm.ts
@@ -41,6 +41,7 @@
  enableYouTubeEmbed: boolean
  enableVideoEmbed: boolean
  enableCheckbox: boolean
  disableBrokenWikilinks: boolean
}
const defaultOptions: Options = {
@@ -56,6 +57,7 @@
  enableYouTubeEmbed: true,
  enableVideoEmbed: true,
  enableCheckbox: false,
  disableBrokenWikilinks: false,
}
const calloutMapping = {
@@ -206,7 +208,7 @@
      return src
    },
    markdownPlugins(_ctx) {
    markdownPlugins(ctx) {
      const plugins: PluggableList = []
      // regex replacements
@@ -275,6 +277,18 @@
                  // otherwise, fall through to regular link
                }
                // treat as broken link if slug not in ctx.allSlugs
                if (opts.disableBrokenWikilinks) {
                  const slug = slugifyFilePath(fp as FilePath)
                  const exists = ctx.allSlugs && ctx.allSlugs.includes(slug)
                  if (!exists) {
                    return {
                      type: "html",
                      value: `<a class=\"internal broken\">${alias ?? fp}</a>`,
                    }
                  }
                }
                // internal link
                const url = fp + anchor
quartz/styles/base.scss
@@ -91,7 +91,7 @@
  color: var(--secondary);
  &:hover {
    color: var(--tertiary) !important;
    color: var(--tertiary);
  }
  &.internal {
@@ -101,6 +101,15 @@
    border-radius: 5px;
    line-height: 1.4rem;
    &.broken {
      color: var(--secondary);
      opacity: 0.5;
      transition: opacity 0.2s ease;
      &:hover {
        opacity: 0.8;
      }
    }
    &:has(> img) {
      background-color: transparent;
      border-radius: 0;