diff --git a/main.js b/main.js index 2957b20..e1694e9 100644 --- a/main.js +++ b/main.js @@ -1,12 +1,14 @@ let Plugin = class {} let MarkdownRenderer = {} let MarkdownRenderChild = class {} +let htmlToMarkdown = (html) => html if (isObsidian()) { const obsidian = require('obsidian') Plugin = obsidian.Plugin MarkdownRenderer = obsidian.MarkdownRenderer MarkdownRenderChild = obsidian.MarkdownRenderChild + htmlToMarkdown = obsidian.htmlToMarkdown } const codeblockId = 'table-of-contents' @@ -159,11 +161,47 @@ function getMarkdownInlineFirstLevelFromHeadings(headings, options) { } function getMarkdownHeading(heading, options) { + const stripMarkdown = (text) => { + text = text.replaceAll('*', '').replaceAll('_', '').replaceAll('`', '') + text = text.replaceAll('==', '').replaceAll('~~', '') + text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Strip markdown links + return text + } + const stripHtml = (text) => stripMarkdown(htmlToMarkdown(text)) + const stripWikilinks = (text, isForLink) => { + // Strip [[link|text]] format + // For the text part of the final link we only keep "text" + // For the link part we need the text + link + // Example: "# Some [[file.md|heading]]" must be translated to "[[#Some file.md heading|Some heading]]" + text = text.replace(/\[\[([^\]]+)\|([^\]]+)\]\]/g, isForLink ? '$1 $2' : '$2') + text = text.replace(/\[\[([^\]]+)\]\]/g, '$1') // Strip [[link]] format + return text + } + const stripTags = (text) => text.replaceAll('#', '') if (options.includeLinks) { - let cleaned = heading.heading - // Strip reserved wikilink characters - cleaned = cleaned.replaceAll('|', '-').replaceAll('[', '{').replaceAll(']', '}') - return `[[#${cleaned}]]` + // Remove markdown, HTML & wikilinks from text for readability, as they are not rendered in a wikilink + let text = heading.heading + text = stripMarkdown(text) + text = stripHtml(text) + text = stripWikilinks(text, false) + // Remove wikilinks & tags from link or it won't be clickable (on the other hand HTML & markdown must stay) + let link = heading.heading + link = stripWikilinks(link, true) + link = stripTags(link) + + // Return wiklink style link + return `[[#${link}|${text}]]` + // Why not markdown links? Because even if it looks like the text part would have a better compatibility + // with complex headings (as it would support HTML, markdown, etc) the link part is messy, + // because it requires some encoding that looks buggy and undocumented; official docs state the link must be URL encoded + // (https://help.obsidian.md/Linking+notes+and+files/Internal+links#Supported+formats+for+internal+links) + // but it doesn't work properly, example: "## Some heading with simple HTML" must be encoded as: + // [Some heading with simple HTML](#Some%20heading%20with%20simpler%20HTML) + // and not + // [Some heading with simple HTML](#Some%20%3Cem%3Eheading%3C%2Fem%3E%20with%20simpler%20HTML) + // Also it won't be clickable at all if the heading contains #tags or more complex HTML + // (example: ## Some heading #with-a-tag) + // (unless there is a way to encode these use cases that I didn't find) } return heading.heading } diff --git a/test/headings.test.js b/test/headings.test.js index 9080c8b..2044b1e 100644 --- a/test/headings.test.js +++ b/test/headings.test.js @@ -4,7 +4,7 @@ const { } = require('../main.js') const testStandardHeadings = [ - { heading: 'Title [1] | level 1', level: 1 }, // With wiklink characters to be stripped + { heading: 'Title 1 level 1', level: 1 }, { heading: 'Title 1 level 2', level: 2 }, { heading: 'Title 1 level 3', level: 3 }, { heading: 'Title 2 level 1', level: 1 }, @@ -21,17 +21,23 @@ const testHeadingsWithoutFirstLevel = [ { heading: 'Title 3 level 3', level: 3 }, ] +const testHeadingsWithSpecialChars = [ + { heading: 'Title 1 `level 1` {with special chars}, **bold**, _italic_, #a-tag, ==highlighted== and ~~strikethrough~~ text', level: 1 }, + { heading: 'Title 1 level 2 with HTML', level: 2 }, + { heading: 'Title 1 level 2 [[wikilink1]] [[wikilink2|wikitext2]] [mdlink](https://mdurl)', level: 2 }, +] + describe('Headings', () => { test('Returns indented list with links', () => { const options = parseOptionsFromSourceText('') const md = getMarkdownFromHeadings(testStandardHeadings, options) const expectedMd = sanitizeMd(` -- [[#Title {1} - level 1]] - - [[#Title 1 level 2]] - - [[#Title 1 level 3]] -- [[#Title 2 level 1]] -- [[#Title 3 level 1]] - - [[#Title 3 level 2]] +- [[#Title 1 level 1|Title 1 level 1]] + - [[#Title 1 level 2|Title 1 level 2]] + - [[#Title 1 level 3|Title 1 level 3]] +- [[#Title 2 level 1|Title 2 level 1]] +- [[#Title 3 level 1|Title 3 level 1]] + - [[#Title 3 level 2|Title 3 level 2]] `) expect(md).toEqual(expectedMd) }) @@ -40,12 +46,12 @@ describe('Headings', () => { const options = parseOptionsFromSourceText('') const md = getMarkdownFromHeadings(testHeadingsWithoutFirstLevel, options) const expectedMd = sanitizeMd(` -- [[#Title 1 level 2]] - - [[#Title 1 level 3]] - - [[#Title 1 level 4]] -- [[#Title 2 level 2]] -- [[#Title 3 level 2]] - - [[#Title 3 level 3]] +- [[#Title 1 level 2|Title 1 level 2]] + - [[#Title 1 level 3|Title 1 level 3]] + - [[#Title 1 level 4|Title 1 level 4]] +- [[#Title 2 level 2|Title 2 level 2]] +- [[#Title 3 level 2|Title 3 level 2]] + - [[#Title 3 level 3|Title 3 level 3]] `) expect(md).toEqual(expectedMd) }) @@ -55,9 +61,9 @@ describe('Headings', () => { options.minLevel = 2 const md = getMarkdownFromHeadings(testStandardHeadings, options) const expectedMd = sanitizeMd(` -- [[#Title 1 level 2]] - - [[#Title 1 level 3]] -- [[#Title 3 level 2]] +- [[#Title 1 level 2|Title 1 level 2]] + - [[#Title 1 level 3|Title 1 level 3]] +- [[#Title 3 level 2|Title 3 level 2]] `) expect(md).toEqual(expectedMd) }) @@ -67,11 +73,11 @@ describe('Headings', () => { options.maxLevel = 2 const md = getMarkdownFromHeadings(testStandardHeadings, options) const expectedMd = sanitizeMd(` -- [[#Title {1} - level 1]] - - [[#Title 1 level 2]] -- [[#Title 2 level 1]] -- [[#Title 3 level 1]] - - [[#Title 3 level 2]] +- [[#Title 1 level 1|Title 1 level 1]] + - [[#Title 1 level 2|Title 1 level 2]] +- [[#Title 2 level 1|Title 2 level 1]] +- [[#Title 3 level 1|Title 3 level 1]] + - [[#Title 3 level 2|Title 3 level 2]] `) expect(md).toEqual(expectedMd) }) @@ -81,7 +87,7 @@ describe('Headings', () => { options.includeLinks = false const md = getMarkdownFromHeadings(testStandardHeadings, options) const expectedMd = sanitizeMd(` -- Title [1] | level 1 +- Title 1 level 1 - Title 1 level 2 - Title 1 level 3 - Title 2 level 1 @@ -97,22 +103,46 @@ describe('Headings', () => { const md = getMarkdownFromHeadings(testStandardHeadings, options) const expectedMd = sanitizeMd(` # My TOC -- [[#Title {1} - level 1]] - - [[#Title 1 level 2]] - - [[#Title 1 level 3]] -- [[#Title 2 level 1]] -- [[#Title 3 level 1]] - - [[#Title 3 level 2]] +- [[#Title 1 level 1|Title 1 level 1]] + - [[#Title 1 level 2|Title 1 level 2]] + - [[#Title 1 level 3|Title 1 level 3]] +- [[#Title 2 level 1|Title 2 level 1]] +- [[#Title 3 level 1|Title 3 level 1]] + - [[#Title 3 level 2|Title 3 level 2]] +`) + expect(md).toEqual(expectedMd) + }) + + test('Returns indented list with sanitized links from special chars', () => { + const options = parseOptionsFromSourceText('') + const md = getMarkdownFromHeadings(testHeadingsWithSpecialChars, options) + const expectedMd = sanitizeMd(` +- [[#Title 1 \`level 1\` {with special chars}, **bold**, _italic_, a-tag, ==highlighted== and ~~strikethrough~~ text|Title 1 level 1 {with special chars}, bold, italic, #a-tag, highlighted and strikethrough text]] + - [[#Title 1 level 2 with HTML|Title 1 level 2 with HTML]] + - [[#Title 1 level 2 wikilink1 wikilink2 wikitext2 [mdlink](https://mdurl)|Title 1 level 2 wikilink1 wikitext2 mdlink]] `) expect(md).toEqual(expectedMd) }) + test('Returns indented list without links from special chars', () => { + const options = parseOptionsFromSourceText('') + options.includeLinks = false + const md = getMarkdownFromHeadings(testHeadingsWithSpecialChars, options) + const expectedMd = sanitizeMd(` +- Title 1 \`level 1\` {with special chars}, **bold**, _italic_, #a-tag, ==highlighted== and ~~strikethrough~~ text + - Title 1 level 2 with HTML + - Title 1 level 2 [[wikilink1]] [[wikilink2|wikitext2]] [mdlink](https://mdurl) +`) + expect(md).toEqual(expectedMd) + }) + + test('Returns flat first-level list with links', () => { const options = parseOptionsFromSourceText('') options.style = 'inlineFirstLevel' const md = getMarkdownFromHeadings(testStandardHeadings, options) const expectedMd = sanitizeMd(` -[[#Title {1} - level 1]] | [[#Title 2 level 1]] | [[#Title 3 level 1]] +[[#Title 1 level 1|Title 1 level 1]] | [[#Title 2 level 1|Title 2 level 1]] | [[#Title 3 level 1|Title 3 level 1]] `) expect(md).toEqual(expectedMd) }) @@ -123,7 +153,7 @@ describe('Headings', () => { options.includeLinks = false const md = getMarkdownFromHeadings(testStandardHeadings, options) const expectedMd = sanitizeMd(` -Title [1] | level 1 | Title 2 level 1 | Title 3 level 1 +Title 1 level 1 | Title 2 level 1 | Title 3 level 1 `) expect(md).toEqual(expectedMd) })