diff --git a/common/data.ts b/common/data.ts index db3db31..4934fa0 100644 --- a/common/data.ts +++ b/common/data.ts @@ -265,6 +265,35 @@ export function getOrderedCustomIdents(value: string) { return value.split(/\s+/g).filter((v) => v); } +/** + * Get the class name for a given position. + * + * @param position The position to get the class name for. + * @param ignoreInsideFlag Whether to ignore the inside flag. + * @returns The class name. + */ +export function getPositionClassName( + position: PostionOptions, + ignoreInsideFlag?: boolean +): string { + switch (position) { + case "before": + return beforeDecoratorClassName; + case "after": + return afterDecoratorClassName; + case "before-inside": + return ignoreInsideFlag + ? beforeDecoratorClassName + : beforeInsideDecoratorClassName; + case "after-inside": + return ignoreInsideFlag + ? afterDecoratorClassName + : afterInsideDecoratorClassName; + default: + return ""; + } +} + /** * Diff level between two numbers. * diff --git a/common/dom.ts b/common/dom.ts deleted file mode 100644 index f27d675..0000000 --- a/common/dom.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - headingDecoratorClassName, - readingHeadingDecoratorClassName, - beforeDecoratorClassName, - beforeInsideDecoratorClassName, - afterDecoratorClassName, - afterInsideDecoratorClassName, -} from "./data"; - -/** - * Query the heading level of an HTML element. - * - * @param element - * @returns Return `-1` for non-heading elements. - */ -export function queryHeadingLevelByElement(element: HTMLElement): number { - switch (element.tagName.toLowerCase()) { - case "h1": - return 1; - case "h2": - return 2; - case "h3": - return 3; - case "h4": - return 4; - case "h5": - return 5; - case "h6": - return 6; - default: - return -1; - } -} - -/** - * Get the class name for a given position. - * - * @param position The position to get the class name for. - * @param ignoreInsideFlag Whether to ignore the inside flag. - * @returns The class name. - */ -export function getPositionClassName( - position: PostionOptions, - ignoreInsideFlag?: boolean -): string { - switch (position) { - case "before": - return beforeDecoratorClassName; - case "after": - return afterDecoratorClassName; - case "before-inside": - return ignoreInsideFlag - ? beforeDecoratorClassName - : beforeInsideDecoratorClassName; - case "after-inside": - return ignoreInsideFlag - ? afterDecoratorClassName - : afterInsideDecoratorClassName; - default: - return ""; - } -} - -/** - * Decorate an HTML element with a given content, opacity and position. - * - * @param element The HTML element to decorate. - * @param content The content to decorate with. - * @param opacity The opacity of the decorator. - * @param position The position of the decorator. - */ -export function decorateHTMLElement( - element: HTMLElement, - content: string, - opacity: OpacityOptions, - position: PostionOptions -): void { - if (content) { - const span = element.createSpan({ - cls: [ - headingDecoratorClassName, - readingHeadingDecoratorClassName, - getPositionClassName(position), - ], - text: content, - attr: { - "data-decorator-opacity": `${opacity}%`, - }, - }); - - if (position === "before-inside") { - const headingCollapseIndicator = element.find( - ".heading-collapse-indicator" - ); - if (headingCollapseIndicator) { - headingCollapseIndicator.after(span); - } else { - const firstChild = element.firstChild; - if (firstChild) { - firstChild.before(span); - } else { - element.appendChild(span); - } - } - } else if (position === "before") { - const firstChild = element.firstChild; - if (firstChild) { - firstChild.before(span); - } else { - element.appendChild(span); - } - } else { - element.appendChild(span); - } - } -} diff --git a/components/plugin.ts b/components/plugin.ts index 56e163f..4e64044 100644 --- a/components/plugin.ts +++ b/components/plugin.ts @@ -11,16 +11,11 @@ import { EditorView, ViewPlugin } from "@codemirror/view"; import type { HeadingPluginSettings, HeadingPluginData } from "../common/data"; import { headingsSelector, - getUnorderedLevelHeadings, - getOrderedCustomIdents, getBoolean, checkEnabledCSS, stringToRegex, defalutSettings, } from "../common/data"; -import { Counter, Querier } from "../common/counter"; -import { Heading } from "../common/heading"; -import { decorateHTMLElement, queryHeadingLevelByElement } from "../common/dom"; import { HeadingEditorViewPlugin, headingDecorationsField, @@ -28,6 +23,7 @@ import { updateEditorMode, } from "./editor"; import { ViewChildComponent } from "./child"; +import { readingOrderedHandler, readingUnorderedHandler } from "./reading"; import { outlineHandler, cancelOutlineDecoration } from "./outline"; import { quietOutlineHandler, @@ -197,24 +193,7 @@ export class HeadingPlugin extends Plugin { return; } - const { - readingSettings: { - opacity, - position, - ordered, - orderedDelimiter, - orderedTrailingDelimiter, - orderedStyleType, - orderedSpecifiedString, - orderedCustomIdents, - orderedIgnoreSingle, - orderedIgnoreMaximum = 6, - orderedBasedOnExisting, - orderedAllowZeroLevel, - unorderedLevelHeadings, - }, - } = this.settings; - + const { ordered } = this.settings.readingSettings; if (ordered) { const file = this.getActiveFile(context.sourcePath); if (!file) { @@ -227,108 +206,14 @@ export class HeadingPlugin extends Plugin { return; } - let ignoreTopLevel = 0; - if (orderedIgnoreSingle || orderedBasedOnExisting) { - const queier = new Querier(orderedAllowZeroLevel); - const heading = new Heading(); - for (let lineIndex = 1; lineIndex <= sourceArr.length; lineIndex++) { - const lineText = sourceArr[lineIndex - 1]; - const nextLineIndex = lineIndex + 1; - const nextLineText = - nextLineIndex <= sourceArr.length - ? sourceArr[nextLineIndex - 1] - : ""; - const level = heading.handler(lineIndex, lineText, nextLineText); - if (level === -1) { - continue; - } - - queier.handler(level); - - ignoreTopLevel = queier.query( - orderedIgnoreSingle, - orderedIgnoreMaximum - ); - if (ignoreTopLevel === 0) { - break; - } - } - } - - const counter = new Counter({ - ordered: true, - delimiter: orderedDelimiter, - trailingDelimiter: orderedTrailingDelimiter, - styleType: orderedStyleType, - customIdents: getOrderedCustomIdents(orderedCustomIdents), - specifiedString: orderedSpecifiedString, - ignoreTopLevel, - allowZeroLevel: orderedAllowZeroLevel, - }); - const heading = new Heading(); - let headingIndex = 1; - - headingElements.forEach((headingElement) => { - const sectionInfo = context.getSectionInfo(headingElement); - if (!sectionInfo) { - return; - } - - const lineStart = sectionInfo.lineStart + 1; - if (lineStart > sourceArr.length) { - return; - } - - for ( - let lineIndex = headingIndex; - lineIndex <= lineStart; - lineIndex++ - ) { - const lineText = sourceArr[lineIndex - 1]; - const nextLineIndex = lineIndex + 1; - const nextLineText = - nextLineIndex <= sourceArr.length - ? sourceArr[nextLineIndex - 1] - : ""; - const level = heading.handler(lineIndex, lineText, nextLineText); - if (lineIndex === lineStart) { - //? 1. When using the page preview feature to only reference fragments, the - //? relative position of rows is incorrect. - //? 2. In the split editing scenario, there may be a delay in the information - //? on the reading tab. - const elementLevel = queryHeadingLevelByElement(headingElement); - if (elementLevel === level) { - const decoratorContent = counter.decorator(level); - decorateHTMLElement( - headingElement, - decoratorContent, - opacity, - position - ); - } - - headingIndex = lineIndex + 1; - } else { - counter.handler(level); - } - } - }); + readingOrderedHandler( + this.settings.readingSettings, + context, + headingElements, + sourceArr + ); } else { - const counter = new Counter({ - ordered: false, - levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings), - }); - - headingElements.forEach((headingElement) => { - const level = queryHeadingLevelByElement(headingElement); - const decoratorContent = counter.decorator(level); - decorateHTMLElement( - headingElement, - decoratorContent, - opacity, - position - ); - }); + readingUnorderedHandler(this.settings.readingSettings, headingElements); } }); diff --git a/components/reading.ts b/components/reading.ts new file mode 100644 index 0000000..ee9bbf8 --- /dev/null +++ b/components/reading.ts @@ -0,0 +1,219 @@ +import { MarkdownPostProcessorContext } from "obsidian"; +import type { HeadingDecoratorSettings } from "../common/data"; +import { + headingDecoratorClassName, + readingHeadingDecoratorClassName, + getOrderedCustomIdents, + getPositionClassName, + getUnorderedLevelHeadings, +} from "../common/data"; +import { Heading } from "../common/heading"; +import { Querier, Counter } from "../common/counter"; + +/** + * Handles ordered headings of reading view. + * + * @param settings The heading decorator settings. + * @param context The markdown post processor context. + * @param headingElements The heading elements to be processed. + * @param sourceArr The source content of the file as an array of lines. + */ +export function readingOrderedHandler( + settings: HeadingDecoratorSettings, + context: MarkdownPostProcessorContext, + headingElements: HTMLElement[], + sourceArr: string[] +): void { + const { + opacity, + position, + orderedDelimiter, + orderedTrailingDelimiter, + orderedStyleType, + orderedSpecifiedString, + orderedCustomIdents, + orderedIgnoreSingle, + orderedIgnoreMaximum = 6, + orderedBasedOnExisting, + orderedAllowZeroLevel, + } = settings; + + let ignoreTopLevel = 0; + if (orderedIgnoreSingle || orderedBasedOnExisting) { + const queier = new Querier(orderedAllowZeroLevel); + const heading = new Heading(); + for (let lineIndex = 1; lineIndex <= sourceArr.length; lineIndex++) { + const lineText = sourceArr[lineIndex - 1]; + const nextLineIndex = lineIndex + 1; + const nextLineText = + nextLineIndex <= sourceArr.length ? sourceArr[nextLineIndex - 1] : ""; + const level = heading.handler(lineIndex, lineText, nextLineText); + if (level === -1) { + continue; + } + + queier.handler(level); + + ignoreTopLevel = queier.query(orderedIgnoreSingle, orderedIgnoreMaximum); + if (ignoreTopLevel === 0) { + break; + } + } + } + + const counter = new Counter({ + ordered: true, + delimiter: orderedDelimiter, + trailingDelimiter: orderedTrailingDelimiter, + styleType: orderedStyleType, + customIdents: getOrderedCustomIdents(orderedCustomIdents), + specifiedString: orderedSpecifiedString, + ignoreTopLevel, + allowZeroLevel: orderedAllowZeroLevel, + }); + const heading = new Heading(); + let headingIndex = 1; + + headingElements.forEach((headingElement) => { + const sectionInfo = context.getSectionInfo(headingElement); + if (!sectionInfo) { + return; + } + + const lineStart = sectionInfo.lineStart + 1; + if (lineStart > sourceArr.length) { + return; + } + + for (let lineIndex = headingIndex; lineIndex <= lineStart; lineIndex++) { + const lineText = sourceArr[lineIndex - 1]; + const nextLineIndex = lineIndex + 1; + const nextLineText = + nextLineIndex <= sourceArr.length ? sourceArr[nextLineIndex - 1] : ""; + const level = heading.handler(lineIndex, lineText, nextLineText); + if (lineIndex === lineStart) { + //? 1. When using the page preview feature to only reference fragments, the + //? relative position of rows is incorrect. + //? 2. In the split editing scenario, there may be a delay in the information + //? on the reading tab. + const elementLevel = queryHeadingLevelByElement(headingElement); + if (elementLevel === level) { + const decoratorContent = counter.decorator(level); + decorateHTMLElement( + headingElement, + decoratorContent, + opacity, + position + ); + } + + headingIndex = lineIndex + 1; + } else { + counter.handler(level); + } + } + }); +} + +/** + * Handles unordered headings of reading view. + * + * @param settings The heading decorator settings. + * @param headingElements The unordered heading elements to be processed. + */ +export function readingUnorderedHandler( + settings: HeadingDecoratorSettings, + headingElements: HTMLElement[] +): void { + const { opacity, position, unorderedLevelHeadings } = settings; + + const counter = new Counter({ + ordered: false, + levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings), + }); + + headingElements.forEach((headingElement) => { + const level = queryHeadingLevelByElement(headingElement); + const decoratorContent = counter.decorator(level); + decorateHTMLElement(headingElement, decoratorContent, opacity, position); + }); +} + +/** + * Query the heading level of an HTML element. + * + * @param element + * @returns Return `-1` for non-heading elements. + */ +function queryHeadingLevelByElement(element: HTMLElement): number { + switch (element.tagName.toLowerCase()) { + case "h1": + return 1; + case "h2": + return 2; + case "h3": + return 3; + case "h4": + return 4; + case "h5": + return 5; + case "h6": + return 6; + default: + return -1; + } +} + +/** + * Decorate an HTML element with a given content, opacity and position. + * + * @param element The HTML element to decorate. + * @param content The content to decorate with. + * @param opacity The opacity of the decorator. + * @param position The position of the decorator. + */ +function decorateHTMLElement( + element: HTMLElement, + content: string, + opacity: OpacityOptions, + position: PostionOptions +): void { + if (content) { + const span = element.createSpan({ + cls: [ + headingDecoratorClassName, + readingHeadingDecoratorClassName, + getPositionClassName(position), + ], + text: content, + attr: { + "data-decorator-opacity": `${opacity}%`, + }, + }); + + if (position === "before-inside") { + const headingCollapseIndicator = element.find( + ".heading-collapse-indicator" + ); + if (headingCollapseIndicator) { + headingCollapseIndicator.after(span); + } else { + const firstChild = element.firstChild; + if (firstChild) { + firstChild.before(span); + } else { + element.appendChild(span); + } + } + } else if (position === "before") { + const firstChild = element.firstChild; + if (firstChild) { + firstChild.before(span); + } else { + element.appendChild(span); + } + } else { + element.appendChild(span); + } + } +} diff --git a/components/weight.ts b/components/weight.ts index 408c027..9a5db03 100644 --- a/components/weight.ts +++ b/components/weight.ts @@ -3,8 +3,8 @@ import { headingDecoratorClassName, previewHeadingDecoratorClassName, sourceHeadingDecoratorClassName, + getPositionClassName, } from "../common/data"; -import { getPositionClassName } from "../common/dom"; export class HeadingWidget extends WidgetType { readonly isLivePreviwMode: boolean;