Louis/.obsidian/plugins/heading-level-indent/main.js
2026-06-09 14:45:18 +02:00

758 lines
No EOL
26 KiB
JavaScript

/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => HeadingIndent
});
module.exports = __toCommonJS(main_exports);
var import_obsidian2 = require("obsidian");
// src/editingMode.ts
var import_language = require("@codemirror/language");
var import_state = require("@codemirror/state");
var import_view = require("@codemirror/view");
// src/FrontmatterListener.ts
var FrontmatterListener = class {
constructor(app) {
this.currentIndentState = null;
this.currentFile = null;
this.listeners = [];
this.fileChanged = false;
this.valueChanged = false;
this.eventRefs = [];
this.app = app;
}
/**
* Update current value and detect whether `heading-indent` has changed
*/
update() {
const prevIndentState = this.currentIndentState;
const prevFile = this.currentFile;
const currentFile = this.app.workspace.getActiveFile();
this.currentFile = currentFile;
if (currentFile) {
const metadata = this.app.metadataCache.getFileCache(currentFile);
const frontmatter = metadata == null ? void 0 : metadata.frontmatter;
this.currentIndentState = (frontmatter == null ? void 0 : frontmatter["heading-indent"]) !== void 0 ? String(frontmatter["heading-indent"]) : "1";
} else {
this.currentIndentState = null;
}
this.fileChanged = prevFile !== currentFile;
this.valueChanged = this.currentIndentState !== prevIndentState;
if (this.valueChanged) {
this.notifyListeners(this.currentIndentState, prevIndentState, currentFile, prevFile);
}
return this.currentIndentState;
}
/**
* Add a listener callback
*/
addListener(callback) {
this.listeners.push(callback);
}
/**
* Remove a listener callback
*/
removeListener(callback) {
const index = this.listeners.indexOf(callback);
if (index > -1) {
this.listeners.splice(index, 1);
}
}
/**
* Notify all registered listeners
*/
notifyListeners(newValue, oldValue, newFile, oldFile) {
this.listeners.forEach((listener) => {
try {
listener(newValue, oldValue, newFile, oldFile);
} catch (error) {
console.error("Listener execution error:", error);
}
});
}
/**
* Determines if heading indentation should be applied to the current file.
*
* Checks frontmatter field 'heading-indent':
* - false/0: disabled
* - Any other value or missing: enabled (default)
*
* @returns true if indentation should be applied
*/
isIndentEnabled() {
if (this.currentIndentState === null) {
return false;
}
const val = String(this.currentIndentState).toLowerCase();
return !(val === "false" || val === "0");
}
/**
* Start listening to workspace and metadata changes
*/
start() {
const leafChangeRef = this.app.workspace.on("active-leaf-change", () => {
this.update();
});
this.eventRefs.push(leafChangeRef);
const metadataChangeRef = this.app.metadataCache.on("changed", (file) => {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile && activeFile.path === file.path) {
this.update();
}
});
this.eventRefs.push(metadataChangeRef);
this.update();
}
/**
* Stop listening and clean up event handlers
*/
stop() {
this.eventRefs.forEach((ref) => this.app.workspace.offref(ref));
this.eventRefs = [];
}
};
var _listener = null;
function initFrontmatterListener(app) {
if (!_listener) {
_listener = new FrontmatterListener(app);
}
}
function getFrontmatterListener() {
if (!_listener) {
throw new Error("FrontmatterListener not initialized");
}
return _listener;
}
function cleanupFrontmatterListener() {
if (_listener) {
_listener.stop();
_listener = null;
}
}
// src/editingMode.ts
var indentStateField = import_state.StateField.define({
create(state) {
return getDecorationSet(state);
},
/**
* lifecicle for an update: DOM event -> transaction -> create new state -> view update
*/
update(currentValue, tr) {
let needUpdate = false;
for (const e of tr.effects) {
if (e.is(updateNeededNotificationEffect)) {
needUpdate = true;
break;
}
}
if (!needUpdate && syntaxTreeChanged(tr)) {
needUpdate = true;
}
if (needUpdate) {
const newValue = getDecorationSet(tr.state);
const isIndentEnabled = getFrontmatterListener().isIndentEnabled();
const newHasDecorations = newValue.decorations.size > 0;
if (isIndentEnabled && !newHasDecorations)
return currentValue;
else
return newValue;
}
return currentValue;
},
provide(field) {
return import_view.EditorView.decorations.from(field, (value) => value.decorations);
}
});
function syntaxTreeChanged(tr) {
const oldTree = (0, import_language.syntaxTree)(tr.startState);
const newTree = (0, import_language.syntaxTree)(tr.state);
return oldTree !== newTree;
}
function getDecorationSet(state) {
var _a;
if (!getFrontmatterListener().isIndentEnabled())
return { decorations: import_view.Decoration.none, intervals: [] };
const settings = window.app.plugins.plugins["heading-level-indent"].settings;
const headings = [];
let highestLevelInDocument = 6;
(0, import_language.syntaxTree)(state).iterate({
enter(node) {
if (node.type.name.startsWith("HyperMD-header_HyperMD-header-")) {
const lineAt = state.doc.lineAt(node.from);
const text = state.doc.sliceString(node.from, node.to);
const level = Number(node.type.name.slice(-1));
const posAt = node.from;
headings.push({
text,
level,
headingLineNumber: lineAt.number,
headingPos: posAt
});
highestLevelInDocument = Math.min(highestLevelInDocument, level);
}
}
});
if (settings.treatHighestPresentHeadingAsH1) {
for (const heading of headings) {
heading.level -= highestLevelInDocument - 1;
}
}
const builder = new import_state.RangeSetBuilder();
const el = document.querySelector(".workspace-leaf.mod-active .cm-content");
if (el === null) return { decorations: import_view.Decoration.none, intervals: [] };
const containerWidth = parseInt(getComputedStyle(el).width);
const intervals = [];
for (const [index, heading] of headings.entries()) {
const { level, headingLineNumber, headingPos } = heading;
const headingLine = state.doc.line(headingLineNumber);
const firstDataLineNumber = headingLineNumber + 1;
const lastDataLineNumber = ((_a = headings[index + 1]) == null ? void 0 : _a.headingLineNumber) - 1 || state.doc.lines;
const pxForDataLine = settings[`h${level}`] || 0;
const pxForHeadingLine = settings[`h${level - 1}` || 0];
intervals.push([headingPos, pxForDataLine]);
const dataStyles = `left:${pxForDataLine}px;width:${containerWidth - pxForDataLine}px;`;
const headingStyles = `left:${pxForHeadingLine}px;width:${containerWidth - pxForHeadingLine}px;`;
builder.add(
headingLine.from,
headingLine.from,
import_view.Decoration.line({
attributes: { style: headingStyles }
})
);
for (let j = firstDataLineNumber; j < lastDataLineNumber + 1; j++) {
const dataLine = state.doc.line(j);
builder.add(
dataLine.from,
dataLine.from,
import_view.Decoration.line({
attributes: { style: dataStyles }
})
);
}
}
return { decorations: builder.finish(), intervals };
}
var indentEmbedsPlugin = import_view.ViewPlugin.fromClass(
class {
constructor(view) {
this.view = view;
this.scheduleIndentEmbedsAfterRender();
}
update(update) {
for (const tr of update.transactions) {
for (const e of tr.effects) {
if (e.is(updateNeededNotificationEffect)) {
this.scheduleIndentEmbedsAfterRender();
}
}
}
if (update.docChanged || update.viewportChanged) {
this.scheduleIndentEmbedsAfterRender();
}
}
scheduleIndentEmbedsAfterRender() {
this.view.requestMeasure({
read: () => {
},
write: (measure, view) => {
const { intervals } = view.state.field(indentStateField);
const dom = this.view.dom;
const el = document.querySelector(".workspace-leaf.mod-active .cm-content");
if (el === null) return;
const containerWidth = parseInt(getComputedStyle(el).width);
const embeds = Array.from(
dom.querySelectorAll("div.cm-content > div.cm-embed-block")
);
const mathBlocks = Array.from(
dom.querySelectorAll("div.cm-content > div.math-block")
);
let prevIntervalIndex = 0;
for (const embed of [...embeds, ...mathBlocks]) {
const [intervalIndex, offset] = findIntervalOffset(
intervals,
view.posAtDOM(embed),
prevIntervalIndex
);
prevIntervalIndex = intervalIndex;
embed.style.left = `${offset}px`;
embed.style.width = `${containerWidth - offset}px`;
}
const internalEmbeds = Array.from(
dom.querySelectorAll("div.cm-content > div.internal-embed")
);
prevIntervalIndex = 0;
for (const embed of internalEmbeds) {
const [intervalIndex, offset] = findIntervalOffset(
intervals,
view.posAtDOM(embed),
prevIntervalIndex
);
prevIntervalIndex = intervalIndex;
embed.style.position = "relative";
embed.style.left = `${offset}px`;
embed.style.width = `${containerWidth - offset}px`;
}
const imgs = Array.from(
dom.querySelectorAll("div.cm-content > img")
);
prevIntervalIndex = 0;
for (const img of imgs) {
const [intervalIndex, offset] = findIntervalOffset(
intervals,
view.posAtDOM(img),
prevIntervalIndex
);
prevIntervalIndex = intervalIndex;
img.style.position = "relative";
img.style.left = `${offset}px`;
img.style.maxWidth = `${containerWidth - offset}px`;
}
}
});
}
}
);
function findIntervalOffset(intervals, pos, start) {
if (!intervals.length || pos < intervals[0][0]) {
return [0, 0];
}
let low = start;
let high = intervals.length - 1;
let result = [0, 0];
while (low <= high) {
const mid = low + high >> 1;
const [midPos, midOffset] = intervals[mid];
if (midPos < pos) {
result = [mid, midOffset];
low = mid + 1;
} else {
high = mid - 1;
}
}
return result;
}
var updateNeededNotificationEffect = import_state.StateEffect.define();
var resizeNotificationPlugin = import_view.ViewPlugin.define((view) => {
let resizeTimeout;
const observer = new ResizeObserver((entries) => {
clearTimeout(resizeTimeout);
resizeTimeout = window.setTimeout(() => {
view.dispatch({
effects: updateNeededNotificationEffect.of()
});
}, 100);
});
observer.observe(view.dom);
return {
destroy() {
observer.disconnect();
}
};
});
// src/renderedMode.ts
var RenderedModeIndenter = class {
/**
* Apply indentation to markdown elements in reading view or PDF export.
* Works by processing the DOM after Obsidian renders the markdown.
*/
static applyIndent(element, settings) {
try {
if (!getFrontmatterListener().isIndentEnabled()) return;
} catch (e) {
console.warn("FrontmatterListener not initialized, applying indent by default");
}
this.currentSettings = settings;
if (this.processingInProgress) return;
const rootContainer = element.closest(".markdown-preview-view");
if (!rootContainer) return;
this.setupObserver(rootContainer, settings);
this.processingInProgress = true;
requestAnimationFrame(() => {
const processor = new IndentProcessor(settings, rootContainer);
processor.process();
this.processingInProgress = false;
});
}
/**
* Set up MutationObserver to watch for DOM changes when scrolling in virtualized documents.
* Obsidian only renders viewport content in large documents, adding/removing nodes as you scroll.
*/
static setupObserver(rootContainer, settings) {
const previewSection = rootContainer.querySelector(".markdown-preview-section");
if (!previewSection) return;
this.setupObserverForSection(previewSection, rootContainer, settings);
}
/**
* Set up a global observer that watches for new preview sections across all markdown views.
* This ensures observers are attached when switching files or opening new views.
*/
static setupGlobalObserver(app, getSettings) {
const workspaceContainer = document.querySelector(".workspace");
if (!workspaceContainer) return;
const globalObserver = new MutationObserver(() => {
const previewSections = document.querySelectorAll(".markdown-preview-section");
previewSections.forEach((section) => {
if (!this.observers.has(section)) {
const rootContainer = section.closest(".markdown-preview-view");
if (rootContainer) {
this.setupObserverForSection(section, rootContainer, getSettings());
}
}
});
});
globalObserver.observe(workspaceContainer, { childList: true, subtree: true });
this.observers.set(workspaceContainer, globalObserver);
const existingSections = document.querySelectorAll(".markdown-preview-section");
existingSections.forEach((section) => {
const rootContainer = section.closest(".markdown-preview-view");
if (rootContainer) {
this.setupObserverForSection(section, rootContainer, getSettings());
}
});
}
/**
* Set up observer for a specific preview section
*/
static setupObserverForSection(previewSection, rootContainer, settings) {
if (this.observers.has(previewSection)) return;
const observer = new MutationObserver((mutations) => {
const hasChildListChanges = mutations.some((m) => m.type === "childList");
if (!hasChildListChanges) return;
if (!this.processingInProgressForObserver) {
this.processingInProgressForObserver = true;
requestAnimationFrame(() => {
const processor = new IndentProcessor(settings, rootContainer);
if (getFrontmatterListener().isIndentEnabled()) {
processor.process();
} else {
processor.clear();
}
this.processingInProgressForObserver = false;
});
}
});
observer.observe(previewSection, { childList: true });
this.observers.set(previewSection, observer);
}
/**
* Remove indentation from all preview views of the current file
*/
static clearCurrentView() {
const allPreviewViews = activeDocument.querySelectorAll(".markdown-preview-view");
allPreviewViews.forEach((previewView) => {
const processor = new IndentProcessor(null, previewView);
processor.clear();
});
}
/**
* Apply indentation to all preview views of the current file
*/
static applyToCurrentView(settings) {
const allPreviewViews = activeDocument.querySelectorAll(".markdown-preview-view");
allPreviewViews.forEach((previewView) => {
const processor = new IndentProcessor(settings, previewView);
processor.process();
});
}
/**
* Disconnect all MutationObservers and clear all indentation (call when plugin unloads)
*/
static cleanup() {
this.observers.forEach((observer) => observer.disconnect());
this.observers.clear();
this.currentSettings = null;
const allPreviewViews = document.querySelectorAll(".markdown-preview-view");
allPreviewViews.forEach((previewView) => {
const processor = new IndentProcessor(null, previewView);
processor.clear();
});
}
};
RenderedModeIndenter.processingInProgress = false;
RenderedModeIndenter.processingInProgressForObserver = false;
RenderedModeIndenter.observers = /* @__PURE__ */ new Map();
RenderedModeIndenter.currentSettings = null;
var IndentProcessor = class {
constructor(settings, rootElement) {
this.settings = settings;
this.rootElement = rootElement;
this.currentHeadingLevel = 0;
this.lastHeadingElement = null;
}
process() {
const selectors = this.getContentSelectors();
const elements = this.rootElement.querySelectorAll(
selectors.map((tag) => `div > ${tag}`).join(", ")
);
for (const element of Array.from(elements)) {
if (this.shouldSkipElement(element)) {
continue;
}
const tagName = element.tagName.toLowerCase();
if (this.isHeading(tagName)) {
this.processHeading(element, tagName);
} else if (this.currentHeadingLevel > 0) {
this.processContent(element);
}
}
}
/**
* Remove all indentation styles and classes
*/
clear() {
const elDivs = this.rootElement.querySelectorAll('div[class*="el-"]');
for (const div of Array.from(elDivs)) {
const htmlDiv = div;
htmlDiv.style.paddingLeft = "";
const classesToRemove = [];
htmlDiv.classList.forEach((className) => {
if (className.startsWith("heading_") || className.startsWith("data_")) {
classesToRemove.push(className);
}
});
classesToRemove.forEach((className) => htmlDiv.classList.remove(className));
}
}
getContentSelectors() {
return [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"p",
"ul",
"ol",
"blockquote",
"table",
"pre",
"div.callout"
];
}
shouldSkipElement(element) {
const isPdfExport = this.rootElement.closest(".print") !== null;
if (isPdfExport && element.classList.contains("__title__")) {
return true;
}
return false;
}
isHeading(tagName) {
return /^h[1-6]$/.test(tagName);
}
processHeading(element, tagName) {
const level = parseInt(tagName.charAt(1));
this.currentHeadingLevel = level;
this.lastHeadingElement = element;
const parentDiv = element.parentElement;
if ((parentDiv == null ? void 0 : parentDiv.tagName) === "DIV") {
const indent = this.getIndentForLevel(level - 1);
parentDiv.style.paddingLeft = `${indent}px`;
parentDiv.classList.add(`heading_h${level}`);
}
}
processContent(element) {
var _a;
const parentDiv = element.parentElement;
if ((parentDiv == null ? void 0 : parentDiv.tagName) === "DIV" && parentDiv !== ((_a = this.lastHeadingElement) == null ? void 0 : _a.parentElement)) {
const indent = this.getIndentForLevel(this.currentHeadingLevel);
parentDiv.style.paddingLeft = `${indent}px`;
parentDiv.classList.add(`data_h${this.currentHeadingLevel}`);
}
}
getIndentForLevel(level) {
if (level === 0) return 0;
const key = `h${level}`;
return parseInt(String(this.settings[key])) || 0;
}
};
// src/settings.ts
var import_obsidian = require("obsidian");
var DEFAULT_SETTINGS = {
h1: "30",
h2: "50",
h3: "70",
h4: "90",
h5: "110",
h6: "130",
treatHighestPresentHeadingAsH1: false
};
var IndentSettingTab = class extends import_obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h3", {
text: "Set indentation for the content of each heading in pixels"
});
containerEl.createEl("div", {
text: `Indentation applied for the heading lines itself will be the same as the
content of inmediately previous heading. For example, if the indentation
for the content of H3 is set to 70 pixels, H2 heading line itself
will be indented the same`,
attr: { style: "margin-bottom: 10px; color: gray;" }
});
new import_obsidian.Setting(containerEl).setName("Content under H1").addText(
(number) => number.setPlaceholder("").setValue(this.plugin.settings.h1).onChange((value) => __async(this, null, function* () {
this.plugin.settings.h1 = value;
yield this.plugin.saveSettings();
}))
);
new import_obsidian.Setting(containerEl).setName("Content under H2").addText(
(text) => text.setPlaceholder("").setValue(this.plugin.settings.h2).onChange((value) => __async(this, null, function* () {
this.plugin.settings.h2 = value;
yield this.plugin.saveSettings();
}))
);
new import_obsidian.Setting(containerEl).setName("Content under H3").addText(
(text) => text.setPlaceholder("").setValue(this.plugin.settings.h3).onChange((value) => __async(this, null, function* () {
this.plugin.settings.h3 = value;
yield this.plugin.saveSettings();
}))
);
new import_obsidian.Setting(containerEl).setName("Content under H4").addText(
(text) => text.setPlaceholder("").setValue(this.plugin.settings.h4).onChange((value) => __async(this, null, function* () {
this.plugin.settings.h4 = value;
yield this.plugin.saveSettings();
}))
);
new import_obsidian.Setting(containerEl).setName("Content under H5").addText(
(text) => text.setPlaceholder("").setValue(this.plugin.settings.h5).onChange((value) => __async(this, null, function* () {
this.plugin.settings.h5 = value;
yield this.plugin.saveSettings();
}))
);
new import_obsidian.Setting(containerEl).setName("Content under H6").addText(
(text) => text.setPlaceholder("").setValue(this.plugin.settings.h6).onChange((value) => __async(this, null, function* () {
this.plugin.settings.h6 = value;
yield this.plugin.saveSettings();
}))
);
new import_obsidian.Setting(containerEl).setName("Treat the highest present heading in the document as H1 (editing mode only)").addToggle(
(toggle) => toggle.setValue(this.plugin.settings.treatHighestPresentHeadingAsH1).onChange((value) => __async(this, null, function* () {
this.plugin.settings.treatHighestPresentHeadingAsH1 = value;
yield this.plugin.saveSettings();
}))
);
}
};
// src/main.ts
var HeadingIndent = class extends import_obsidian2.Plugin {
// Configure resources needed by the plugin.
onload() {
return __async(this, null, function* () {
yield this.loadSettings();
this.addSettingTab(new IndentSettingTab(this.app, this));
initFrontmatterListener(this.app);
getFrontmatterListener().start();
this.registerEditorExtension(indentStateField);
this.registerEditorExtension(indentEmbedsPlugin);
this.registerEditorExtension(resizeNotificationPlugin);
this.registerMarkdownPostProcessor((element, _context) => {
RenderedModeIndenter.applyIndent(element, this.settings);
}, 10);
this.frontmatterChangeHandler = (_newValue, _oldValue, _newFile, _oldFile) => {
if (getFrontmatterListener().isIndentEnabled()) {
RenderedModeIndenter.applyToCurrentView(this.settings);
} else {
RenderedModeIndenter.clearCurrentView();
}
this.app.workspace.iterateAllLeaves((leaf) => {
var _a;
const view = (_a = leaf.view.editor) == null ? void 0 : _a.cm;
if (view) {
view.dispatch({
effects: updateNeededNotificationEffect.of()
});
}
});
};
getFrontmatterListener().addListener(this.frontmatterChangeHandler);
this.app.workspace.onLayoutReady(() => {
RenderedModeIndenter.setupGlobalObserver(this.app, () => this.settings);
});
});
}
/**
* Release any resources configured by the plugin; Automatically clean up registered event listeners
*/
onunload() {
if (this.frontmatterChangeHandler) {
getFrontmatterListener().removeListener(this.frontmatterChangeHandler);
}
cleanupFrontmatterListener();
RenderedModeIndenter.cleanup();
}
loadSettings() {
return __async(this, null, function* () {
this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
});
}
saveSettings() {
return __async(this, null, function* () {
yield this.saveData(this.settings);
this.app.workspace.iterateAllLeaves((leaf) => {
var _a;
const view = (_a = leaf.view.editor) == null ? void 0 : _a.cm;
if (view) {
view.dispatch({
effects: updateNeededNotificationEffect.of()
});
}
});
});
}
};
/* nosourcemap */