refactor(reading): extract the processing code of the reading view into a file

This commit is contained in:
dragonish 2025-05-29 12:53:32 +08:00
parent 55729c1e13
commit d420ddd3e2
No known key found for this signature in database
GPG key ID: 6F42FA9E807A5177
5 changed files with 258 additions and 241 deletions

View file

@ -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.
*

View file

@ -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);
}
}
}

View file

@ -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);
}
});

219
components/reading.ts Normal file
View file

@ -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);
}
}
}

View file

@ -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;