dragonish_obsidian-heading-.../components/reading.ts
dragonish c0bd1e15aa
feat: add a new independent decorator mode
Resolve: #22
BREAKING CHANGE: adjust some settings, introduce a new decorator mode setting.
2026-01-09 13:04:07 +08:00

292 lines
8.1 KiB
TypeScript

import { MarkdownPostProcessorContext } from "obsidian";
import type { HeadingDecoratorSettings } from "../common/data";
import {
className,
getOrderedCustomIdents,
getPositionClassName,
getUnorderedLevelHeadings,
} from "../common/data";
import { Heading } from "../common/heading";
import {
Querier,
UnorderedCounter,
OrderedCounter,
IndependentCounter,
} 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 {
decoratorMode = "orderd",
opacity,
position,
maxRecLevel,
orderedDelimiter,
orderedTrailingDelimiter,
orderedCustomTrailingDelimiter,
orderedLeadingDelimiter,
orderedCustomLeadingDelimiter,
orderedStyleType,
orderedSpecifiedString,
orderedCustomIdents,
orderedIgnoreSingle,
orderedIgnoreMaximum = 6,
orderedAlwaysIgnore,
orderedBasedOnExisting,
orderedAllowZeroLevel,
independentSettings,
} = settings;
let ignoreTopLevel = 0;
const ignoreSingle = !orderedAlwaysIgnore && orderedIgnoreSingle;
const ignoreLimit = orderedAlwaysIgnore ? orderedIgnoreMaximum : 0;
if (ignoreSingle || orderedBasedOnExisting) {
const queier = new Querier(orderedAllowZeroLevel, maxRecLevel);
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(ignoreSingle, orderedIgnoreMaximum);
if (ignoreTopLevel <= ignoreLimit) {
break;
}
}
}
if (ignoreTopLevel < ignoreLimit) {
ignoreTopLevel = ignoreLimit;
}
let counter: Counter;
if (decoratorMode === "independent") {
counter = new IndependentCounter({
maxRecLevel,
ignoreTopLevel,
allowZeroLevel: orderedAllowZeroLevel,
orderedRecLevel: independentSettings?.orderedRecLevel,
h1: independentSettings?.h1,
h2: independentSettings?.h2,
h3: independentSettings?.h3,
h4: independentSettings?.h4,
h5: independentSettings?.h5,
h6: independentSettings?.h6,
});
} else {
counter = new OrderedCounter({
maxRecLevel,
ignoreTopLevel,
allowZeroLevel: orderedAllowZeroLevel,
delimiter: orderedDelimiter,
trailingDelimiter: orderedTrailingDelimiter,
customTrailingDelimiter: orderedCustomTrailingDelimiter,
leadingDelimiter: orderedLeadingDelimiter,
customLeadingDelimiter: orderedCustomLeadingDelimiter,
styleType: orderedStyleType,
customIdents: getOrderedCustomIdents(orderedCustomIdents),
specifiedString: orderedSpecifiedString,
});
}
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,
level
);
}
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, maxRecLevel, unorderedLevelHeadings } = settings;
const counter = new UnorderedCounter(
getUnorderedLevelHeadings(unorderedLevelHeadings),
maxRecLevel
);
headingElements.forEach((headingElement) => {
const level = queryHeadingLevelByElement(headingElement);
const decoratorContent = counter.decorator(level);
decorateHTMLElement(
headingElement,
decoratorContent,
opacity,
position,
level
);
});
}
/**
* 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.
* @param level The level of the heading.
*/
function decorateHTMLElement(
element: HTMLElement,
content: string,
opacity: OpacityOptions,
position: PostionOptions,
level: number
): void {
const decoratorEle = element.find(`.${className.reading}`);
if (content) {
const positionClassName = getPositionClassName(position);
if (decoratorEle) {
if (decoratorEle.classList.contains(positionClassName)) {
decoratorEle.textContent = content;
decoratorEle.dataset.decoratorOpacity = `${opacity}%`;
decoratorEle.dataset.decoratorLevel = level.toString();
return;
} else {
decoratorEle.remove();
}
}
const span = element.createSpan({
cls: [
className.heading,
className.reading,
getPositionClassName(position),
],
text: content,
attr: {
"data-decorator-opacity": `${opacity}%`,
"data-decorator-level": level.toString(),
},
});
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);
}
} else {
decoratorEle && decoratorEle.remove();
}
}
/**
* Cancel a decorator from an HTML element.
*
* @param element The HTML element to cancel the decorator.
*/
export function cancelHTMLDecorator(element: HTMLElement): void {
const decoratorEle = element.find(`.${className.reading}`);
decoratorEle && decoratorEle.remove();
}