UNPKG

insta-toc

Version:

Simultaneously generate, update, and maintain a table of contents for your notes in real time.

195 lines (172 loc) 7.87 kB
import type { HeadingCache } from "obsidian"; import { describe, expect, test } from "vitest"; import { TocModel } from "../src/tocModel"; import type { FileKey, TocBlockItem, TocBlockModel } from "../src/types"; import { createTocModelPluginMock } from "./mocks/pluginClassMocks"; import { testHeadingsMixed, testHeadingsWithSpecialChars, testHeadingsWithoutFirstLevel, testOmitHeadingRegex, testStandardHeadings } from "./mocks/testingObjects"; type ExpectedTocItem = { text: string; href: string; children: ExpectedTocItem[]; }; function testGetIndentationLevel( headingLevel: number, headingLevelStack: number[] ): { currentIndentLevel: number; headingLevelStack: number[]; } { // Pop from the stack until we find a heading level less than the current while ( headingLevelStack.length > 0 // Avoid indentation for the first heading && headingLevel <= headingLevelStack[headingLevelStack.length - 1] ) { headingLevelStack.pop(); } headingLevelStack.push(headingLevel); const currentIndentLevel = headingLevelStack.length - 1; return { currentIndentLevel, headingLevelStack }; } function buildTocSource(headings: HeadingCache[]): string { let headingLevelStack: number[] = []; return headings .map((headingCache) => { const { currentIndentLevel, headingLevelStack: nextStack } = testGetIndentationLevel( headingCache.level, headingLevelStack ); headingLevelStack = nextStack; return `${" ".repeat(currentIndentLevel * 4)}- ${headingCache.heading}`; }) .join("\n"); } function createModel(source: string, sourceFilePath: FileKey): TocBlockModel { const { app, settings, uiStateManager } = createTocModelPluginMock(); return new TocModel( uiStateManager, app, settings, { localSettings: source, sourceFilePath }, (key) => uiStateManager.getTocFoldState(key) ) .model; } function toExpectedItems(items: TocBlockItem[]): ExpectedTocItem[] { return items.map((item) => ({ text: item.text, href: item.href, children: toExpectedItems(item.children) })); } function buildRenderedItems(headings: HeadingCache[], fileName: string): ExpectedTocItem[] { const filteredHeadings = headings.filter(({ heading }) => !testOmitHeadingRegex.test(heading)); const source = buildTocSource(filteredHeadings); const model = createModel(source, `${fileName}.md`); return toExpectedItems(model.items); } describe("TocModel headings", () => { test("builds nested items from ordered heading levels", () => { // Arrange & Act const items = buildRenderedItems(testStandardHeadings, "testStandardHeadings"); // Assert expect(items).toEqual([ { text: "Title 1 Level 1", href: "testStandardHeadings#Title 1 Level 1", children: [ { text: "Title 1 Level 2", href: "testStandardHeadings#Title 1 Level 2", children: [ { text: "Title 1 Level 3", href: "testStandardHeadings#Title 1 Level 3", children: [ { text: "Title 1 Level 4", href: "testStandardHeadings#Title 1 Level 4", children: [ { text: "Title 1 Level 5", href: "testStandardHeadings#Title 1 Level 5", children: [ { text: "Title 1 Level 6", href: "testStandardHeadings#Title 1 Level 6", children: [] } ] } ] } ] } ] } ] } ]); }); test("builds nested items when the first heading is not level one", () => { // Arrange & Act const items = buildRenderedItems(testHeadingsWithoutFirstLevel, "testHeadingsWithoutFirstLevel"); // Assert expect(items).toEqual([ { text: "Title 1 Level 2", href: "testHeadingsWithoutFirstLevel#Title 1 Level 2", children: [ { text: "Title 1 Level 3", href: "testHeadingsWithoutFirstLevel#Title 1 Level 3", children: [ { text: "Title 1 Level 4", href: "testHeadingsWithoutFirstLevel#Title 1 Level 4", children: [ { text: "Title 1 Level 5", href: "testHeadingsWithoutFirstLevel#Title 1 Level 5", children: [ { text: "Title 1 Level 6", href: "testHeadingsWithoutFirstLevel#Title 1 Level 6", children: [] } ] } ] } ] } ] } ]); }); test("builds sibling branches from disorderly heading levels", () => { // Arrange & Act const items = buildRenderedItems(testHeadingsMixed, "testHeadingsMixed"); // Assert expect(items).toEqual([ { text: "Title 1 Level 4", href: "testHeadingsMixed#Title 1 Level 4", children: [] }, { text: "Title 1 Level 1", href: "testHeadingsMixed#Title 1 Level 1", children: [ { text: "Title 1 Level 6", href: "testHeadingsMixed#Title 1 Level 6", children: [] }, { text: "Title 1 Level 2", href: "testHeadingsMixed#Title 1 Level 2", children: [] }, { text: "Title 2 Level 2", href: "testHeadingsMixed#Title 2 Level 2", children: [ { text: "Title 1 Level 3", href: "testHeadingsMixed#Title 1 Level 3", children: [] } ] } ] } ]); }); test("sanitizes rendered link text while preserving transformed href targets", () => { // Arrange & Act const items = buildRenderedItems(testHeadingsWithSpecialChars, "testHeadingsWithSpecialChars"); // Assert expect(items).toEqual([ { text: "Title 1 level 1 with special chars, bold, italic, a-tag, highlighted and strikethrough text", href: "testHeadingsWithSpecialChars#Title 1 `level 1` {with special chars}, **bold**, _italic_, a-tag, ==highlighted== and ~~strikethrough~~ text", children: [ { text: "Title 1 level 2 with HTML", href: "testHeadingsWithSpecialChars#Title 1 level 2 <em style=\"color: black\">with HTML</em>", children: [ { text: "Title 1 level 3 wikilink1 wikitext2 mdlink", href: "testHeadingsWithSpecialChars#Title 1 level 3 wikilink1 wikilink2 wikitext2 [mdlink](https://mdurl)", children: [ { text: "Title 1 level 4 wikilink1 wikitext2 mdlink1 wikilink3 wikitext3 mdlink2", href: "testHeadingsWithSpecialChars#Title 1 level 4 wikilink1 wikilink2 wikitext2 [mdlink1](https://mdurl) wikilink3 wikilink4 wikitext3 [mdlink2](https://mdurl)", children: [] } ] } ] } ] } ]); }); test("falls back to contentText when a markdown link alias is empty", () => { // Arrange & Act const model = createModel("- [](https://example.com)", "empty-markdown-alias.md"); // Assert expect(toExpectedItems(model.items)).toEqual([ { text: "[](https://example.com)", href: "empty-markdown-alias#[](https://example.com)", children: [] } ]); }); });