想实现类似word里的这种中文自动编号,每个层级自动按当前层级的规则来编号。找了全网都没有合适的方案。请教大家
number headings有在用,但是都是数字编号,不满足日常的中文笔记习惯。前阵子研究了改造number headings,失败告终。。
下面的是我修改的number headings 可以让AI去改造一下.只要你提前确定好 不同标题级别的逻辑. 应该很好改造的.
'use strict';
var obsidian = require('obsidian');
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
// 基础工具函数
function getActiveView(app) {
const activeView = app.workspace.getActiveViewOfType(obsidian.MarkdownView);
return activeView !== null && activeView !== void 0 ? activeView : undefined;
}
function isViewActive(app) {
const activeView = getActiveView(app);
return !!(activeView && activeView.file);
}
function getViewInfo(app) {
const activeView = getActiveView(app);
if (!activeView || !activeView.file) return undefined;
const data = app.metadataCache.getFileCache(activeView.file) || {};
const editor = activeView.editor;
if (activeView && data && editor) {
return { activeView, data, editor };
}
return undefined;
}
// 添加大写和小写字母转换函数
function numberToUpperLetter(num) {
let result = '';
while (num > 0) {
num--;
result = String.fromCharCode(65 + (num % 26)) + result; // 65 是大写 'A' 的 ASCII 码
num = Math.floor(num / 26);
}
return result;
}
function numberToLowerLetter(num) {
let result = '';
while (num > 0) {
num--;
result = String.fromCharCode(97 + (num % 26)) + result; // 97 是小写 'a' 的 ASCII 码
num = Math.floor(num / 26);
}
return result;
}
// 罗马数字转换
function numberToRoman(num) {
const romanNumerals = [
{ value: 1000, symbol: 'M' },
{ value: 900, symbol: 'CM' },
{ value: 500, symbol: 'D' },
{ value: 400, symbol: 'CD' },
{ value: 100, symbol: 'C' },
{ value: 90, symbol: 'XC' },
{ value: 50, symbol: 'L' },
{ value: 40, symbol: 'XL' },
{ value: 10, symbol: 'X' },
{ value: 9, symbol: 'IX' },
{ value: 5, symbol: 'V' },
{ value: 4, symbol: 'IV' },
{ value: 1, symbol: 'I' }
];
let result = '';
for (let i = 0; i < romanNumerals.length; i++) {
while (num >= romanNumerals[i].value) {
result += romanNumerals[i].symbol;
num -= romanNumerals[i].value;
}
}
return result;
}
// 编号转换函数
function convertNumber(num, style, isTopLevel) {
switch (style) {
case '1': return num.toString();
case 'A': return isTopLevel ? numberToUpperLetter(num) : numberToLowerLetter(num);
case 'I': return numberToRoman(num);
default: return num.toString();
}
}
// 编号生成逻辑
function generateNumbering(numbers, level, settings) {
const parts = [];
if (level === 0) {
// 最高级别使用 styleLevel1
parts.push(convertNumber(numbers[0], settings.styleLevel1, true));
} else {
// 第一个数字使用 styleLevel1,其他使用 styleLevelOther
parts.push(convertNumber(numbers[0], settings.styleLevel1, true));
for (let i = 1; i <= level; i++) {
parts.push(convertNumber(numbers[i], settings.styleLevelOther, false));
}
}
return parts.join('.');
}
// 优化后的标题清理函数
function cleanHeadingText(text) {
const trimmedText = text.trim();
// 修改后的日期时间格式检查: 检查是否以日期时间开头
const dateTimePrefixRegex = /^(\d{4}年\d{1,2}月\d{1,2}日(?:\s+\d{1,2}:\d{1,2}(?::\d{1,2})?)?\s*)/;
const match = trimmedText.match(dateTimePrefixRegex);
let prefix = '';
let textToClean = trimmedText;
if (match) {
prefix = match[1]; // 提取日期时间前缀
textToClean = trimmedText.substring(prefix.length).trim(); // 获取需要清理的剩余部分
// 如果清理后剩余部分为空,直接返回前缀
if (textToClean === '') {
return prefix.trim();
}
} else {
// 如果文本很短或没有特殊字符(且不含日期前缀),直接返回
if (text.length < 3) return text;
}
// 如果在提取日期前缀后,剩余文本为空,则无需继续清理
if (match && textToClean === '') {
return prefix.trim();
}
// 创建一个正则表达式数组,避免重复创建
const patterns = [
/\*\*/g, // 加粗
/[\u{1F300}-\u{1F9FF}\u{2000}-\u{2BFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}]/gu, // 表情
/^\s*\([^)]*\)\s*/, // 括号 (应用于 textToClean 的开头)
/^\s*\d+(?:\.\d+)*\.?\s*/, // 阿拉伯数字 (应用于 textToClean 的开头)
/^\s*[一二三四五六七八九十百千万]+[、.]\s*/, // 中文数字 (应用于 textToClean 的开头)
/^\s*[ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ]+[、.]\s*/, // 罗马数字 (应用于 textToClean 的开头)
/`/g, // 代码格式
/\s{2,}/g, // 多余空格
/^\s*[\d.]+\s*[::]\s*/, // 数字后面的冒号 (应用于 textToClean 的开头)
/^\s*[::]\s*/, // 开头的冒号 (应用于 textToClean 的开头)
/\s*[::]\s*/ // 任意位置的冒号
];
// 对 textToClean 应用清理规则
let cleanedText = textToClean;
let changed;
// 最多循环3次,避免死循环
for (let i = 0; i < 3; i++) {
changed = false;
const originalLength = cleanedText.length;
// 应用所有清理模式
for (const pattern of patterns) {
const newText = cleanedText.replace(pattern, '');
if (newText !== cleanedText) {
cleanedText = newText;
changed = true;
}
}
// 如果没有变化,提前退出
if (!changed) break;
}
// 返回前缀 + 清理后的文本
// 在拼接前确保 cleanedText 不是空的,避免 "prefix " 这样的结果
const result = prefix + (cleanedText.trim() === '' ? '' : cleanedText.trim());
return result.trim(); // 最后再 trim 一次
}
// 修改后的 updateHeadingNumbering 函数
function updateHeadingNumbering(viewInfo, settings) {
if (!viewInfo) return;
const headings = viewInfo.data.headings ?? [];
if (headings.length === 0) return;
const editor = viewInfo.editor;
const minLevel = Math.min(...headings.map(h => h.level));
let currentNumbers = new Array(6).fill(0);
const changes = [];
let modifiedCount = 0;
for (const heading of headings) {
const level = heading.level;
const relativeLevel = level - minLevel;
currentNumbers[relativeLevel]++;
for (let i = relativeLevel + 1; i < 6; i++) {
currentNumbers[i] = 0;
}
const numberParts = generateNumbering(
currentNumbers.slice(0, relativeLevel + 1),
relativeLevel,
settings
);
const lineText = editor.getLine(heading.position.start.line);
const headingMatch = lineText.match(/^(\s{0,4}#{1,6})(\s+(?:[A-Za-z0-9IVXLCDM]+\.)*[A-Za-z0-9IVXLCDM]+\s+|\s+)(.*)/);
if (headingMatch) {
const [, hashPart, existingSpace, restOfLine] = headingMatch;
// 在这里应用格式化
const cleanedText = cleanHeadingText(restOfLine);
const newLine = `${hashPart} ${numberParts} ${cleanedText}`;
if (lineText !== newLine) {
changes.push({
from: { line: heading.position.start.line, ch: 0 },
to: { line: heading.position.start.line, ch: lineText.length },
text: newLine
});
modifiedCount++;
}
}
}
if (changes.length > 0) {
editor.transaction({ changes });
// 添加控制台日志
console.log('标题编号插件: 更新了 ' + modifiedCount + ' 个标题');
// 添加通知
const fileName = viewInfo.activeView.file.basename;
new obsidian.Notice(`已更新 ${fileName} 中的 ${modifiedCount} 个标题`);
}
}
const DEFAULT_SETTINGS = {
styleLevel1: '1',
styleLevelOther: '1',
auto: false,
off: false,
refreshInterval: 10, // 自动刷新间隔(秒)
updateDelay: 1 // 编辑后更新延时(秒)
};
class NumberHeadingsPlugin extends obsidian.Plugin {
constructor() {
super(...arguments);
this.settings = DEFAULT_SETTINGS;
this.updateTimeout = null;
}
onload() {
return __awaiter(this, void 0, void 0, function* () {
yield this.loadSettings();
// 添加编辑器变更事件监听器
this.registerEvent(
this.app.workspace.on('editor-change', (editor) => {
if (this.settings.auto && !this.settings.off) {
const viewInfo = getViewInfo(this.app);
if (viewInfo) {
// 使用防抖来避免频繁更新
if (this.updateTimeout) {
clearTimeout(this.updateTimeout);
}
this.updateTimeout = setTimeout(() => {
updateHeadingNumbering(viewInfo, this.settings);
}, this.settings.updateDelay * 1000); // 转换为毫秒
}
}
})
);
// 添加命令
this.addCommand({
id: 'number-headings',
name: '对文档中的所有标题进行编号',
checkCallback: (checking) => {
if (checking) return isViewActive(this.app);
const viewInfo = getViewInfo(this.app);
if (viewInfo && !this.settings.off) {
updateHeadingNumbering(viewInfo, this.settings);
}
return false;
}
});
this.addCommand({
id: 'remove-number-headings',
name: '删除文档中所有标题的编号',
checkCallback: (checking) => {
if (checking) return isViewActive(this.app);
const viewInfo = getViewInfo(this.app);
if (viewInfo) {
const changes = [];
for (const heading of viewInfo.data.headings ?? []) {
const lineText = viewInfo.editor.getLine(heading.position.start.line);
const match = lineText.match(/^(\s{0,4}#{1,6})\s+(?:[A-Za-z0-9IVXLCDM]+\.)*[A-Za-z0-9IVXLCDM]+\s+/);
const headingStart = lineText.match(/^(\s{0,4}#{1,6})\s*/);
if (match) {
changes.push({
from: {
line: heading.position.start.line,
ch: headingStart[0].length
},
to: {
line: heading.position.start.line,
ch: match[0].length
},
text: ' '
});
}
}
if (changes.length > 0) {
viewInfo.editor.transaction({ changes });
}
}
return true;
}
});
this.addSettingTab(new NumberHeadingsPluginSettingTab(this.app, this));
// 注册自动编号定时器
this.registerInterval(window.setInterval(() => {
const viewInfo = getViewInfo(this.app);
if (viewInfo && this.settings.auto && !this.settings.off) {
updateHeadingNumbering(viewInfo, this.settings);
}
}, this.settings.refreshInterval * 1000));
});
}
onunload() {
if (this.updateTimeout) {
clearTimeout(this.updateTimeout);
}
}
loadSettings() {
return __awaiter(this, void 0, void 0, function* () {
this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
});
}
saveSettings() {
return __awaiter(this, void 0, void 0, function* () {
yield this.saveData(this.settings);
});
}
}
class NumberHeadingsPluginSettingTab extends obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: '标题编号-设置' });
new obsidian.Setting(containerEl)
.setName('一级标题的样式')
.setDesc('定义一级标题的编号样式。有效值为:1(数字)、A(字母)或 I(罗马数字)')
.addDropdown(dropdown => dropdown
.addOption('1', '数字 (1, 2, 3)')
.addOption('A', '字母 (A, B, C)')
.addOption('I', '罗马数字 (I, II, III)')
.setValue(this.plugin.settings.styleLevel1)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.styleLevel1 = value;
yield this.plugin.saveSettings();
})));
new obsidian.Setting(containerEl)
.setName('较低级别标题样式')
.setDesc('定义较低级别标题的编号样式。有效值为:1(数字)、A(字母)或 I(罗马数字)')
.addDropdown(dropdown => dropdown
.addOption('1', '数字 (1, 2, 3)')
.addOption('A', '字母 (a, b, c)')
.addOption('I', '罗马数字 (I, II, III)')
.setValue(this.plugin.settings.styleLevelOther)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.styleLevelOther = value;
yield this.plugin.saveSettings();
})));
new obsidian.Setting(containerEl)
.setName('自动编号')
.setDesc('开启文档的自动编号')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.auto)
.setTooltip('启用自动编号')
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.auto = value;
yield this.plugin.saveSettings();
})));
new obsidian.Setting(containerEl)
.setName('自动刷新间隔')
.setDesc('自动编号的刷新间隔(秒)')
.addSlider(slider => slider
.setLimits(1, 60, 1)
.setValue(this.plugin.settings.refreshInterval)
.setDynamicTooltip()
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.refreshInterval = value;
yield this.plugin.saveSettings();
})));
new obsidian.Setting(containerEl)
.setName('编辑响应延时')
.setDesc('编辑后更新编号的延时(秒)。较小的值响应更快,但可能会影响编辑体验。')
.addSlider(slider => slider
.setLimits(1, 60, 1) // 范围改为1-60秒,步进值为1
.setValue(this.plugin.settings.updateDelay)
.setDynamicTooltip()
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.updateDelay = value;
yield this.plugin.saveSettings();
})));
}
}
module.exports = NumberHeadingsPlugin;