Files
server-configs/siyuan/data/plugins/headingIndex/index.js

976 lines
31 KiB
JavaScript
Raw Normal View History

2026-02-13 22:24:27 +08:00
const { Plugin } = require("siyuan");
const clientApi = require("siyuan");
let 核心api;
let path;
let 思源工作空间;
let importDep;
let 当前选项按钮;
let that;
let template;
class headingIndex extends Plugin {
onload() {
this.selfURL = `/plugins/${this.constructor.name}`;
this.dataPath = `/data/storage/petal/${this.constructor.name}`;
that = this;
this.设置字典 = {};
this.当前默认设置 = [];
this.已提示块 = [];
this.生成顶栏();
this.增加编辑器生成菜单();
this.添加页面();
this.初始化();
}
增加编辑器生成菜单() {
this.eventBus.on("click-editortitleicon", (e) => {
let { menu, data } = e.detail;
menu.addItem({
icon: "iconOrderedList",
label: this.i18n.设置序号生成方式,
submenu: [
{
icon: "iconOrderedList",
label: this.i18n.刷新序号,
click: () => {
生成标题序号(that.设置字典, data.id);
},
},
{
icon: "iconEdit",
label: this.i18n.写入序号,
click: async () => {
clientApi.confirm(
"⚠️",
this.i18n["生成可能需要很长时间,是否确认继续?"],
async () => {
await 生成文档内标题序号(data.id, that.设置字典, true);
}
);
},
},
],
});
let _submenu = [];
Object.getOwnPropertyNames(this.设置字典).forEach((item) => {
if (item == "当前全局配置") {
return;
}
_submenu.push({
label: this.i18n.使用序号类型 + item,
click: async () => {
await 核心api.setBlockAttrs({
id: data.id,
attrs: { "custom-index-scheme": item },
});
},
});
});
menu.addItem({
icon: "iconOrderedList",
label: this.i18n.选择序号类型,
submenu: _submenu,
});
menu.addItem({
icon: "iconRefresh",
label:(this.自动刷新标题?"结束":"开始")+this.i18n.自动刷新标题,
click:()=>{
this.自动刷新标题=!this.自动刷新标题
}
})
});
}
添加页面() {
let plugin = this;
this.customTab = this.addTab({
type: "editor",
init() {
this.data.content.forEach((标题样式, i) => {
console.log(i);
this.element.insertAdjacentHTML(
"beforeend",
`<label class="fn__flex b3-label">
<div class="fn__flex-1">
h${i + 1}
<div class="b3-label__text">${
plugin.i18n[数字转中文(i + 1) + "级标题编号样式"]
}</div>
</div>
<span class="fn__space"></span>
<input class="b3-text-field fn__flex-center" data-level="${i}" >
</label>`
);
this.element.querySelector(`[data-level="${i}"]`).value = 标题样式;
this.element
.querySelector(`[data-level="${i}"]`)
.addEventListener("change", async (e) => {
this.data.content[i] = e.target.value;
});
});
this.element.insertAdjacentHTML(
"beforeend",
`
<label class="fn__flex b3-label config__item">
<div class="fn__flex-1">
保存配置文件
<div class="b3-label__text">${this.data.name}.json</div>
</div>
<div class="fn__space"></div>
<div class="fn__size200 config__item-line fn__flex-center">
<button class="b3-button b3-button--outline fn__size200 fn__flex-center" >
确定
</button>
</div>
</label>
`
);
this.element.querySelectorAll(`button`).forEach((button) => {
button.addEventListener("click", async (e) => {
if (e.target.dataSet && e.target.dataSet.enable) {
await 思源工作空间.writeFile(
JSON.stringify(
{ name: this.data.name, content: this.data.content },
undefined,
2
),
path.join(plugin.dataPath, "lastValue.json")
);
}
await 思源工作空间.writeFile(
JSON.stringify(this.data.content, undefined, 2),
path.join(plugin.dataPath, this.data.name + ".json")
);
await plugin.初始化();
e.stopPropagation();
});
});
},
async destroy() {
await 思源工作空间.writeFile(
JSON.stringify(this.data.content, undefined, 2),
path.join(plugin.dataPath, this.data.name + ".json")
);
await plugin.初始化();
},
});
}
onunload() {
this.停止监听编辑();
this.样式元素.remove();
}
停止监听编辑() {
this.eventBus.off("ws-main", this.ws监听器);
}
async 初始化() {
path = (await import(this.selfURL + "/polyfills/path.js"))["default"];
importDep = async (moduleName) => {
return await import(path.join(this.selfURL, moduleName));
};
核心api = (await importDep("./polyfills/kernelApi.js"))["default"];
思源工作空间 = (await importDep("./polyfills/fs.js"))["default"];
await this.覆盖默认设置();
await this.获取全部设置();
document
.querySelectorAll("#headingIndexStyle")
.forEach((el) => el.remove());
this.样式元素 = document.createElement("style");
this.样式元素.setAttribute("id", "headingIndexStyle");
this.样式元素.textContent = `
.protyle-wysiwyg [data-node-id].li[data-subtype="t"] .protyle-action.protyle-action--task:before {
content:var(--custom-index) ;
}
.protyle-wysiwyg [data-type="NodeHeading"] [contenteditable]:before{
content:var(--custom-index);
}
.sy__outline [data-node-id] .b3-list-item__text:before{
content:var(--custom-index);
}
`;
let scriptEl = document.createElement("script");
scriptEl.textContent = await (
await fetch(path.join(this.selfURL, "static", "art-template-web.js"))
).text();
let iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.setAttribute("href", "about:blank");
document.body.appendChild(iframe);
iframe.contentDocument.head.appendChild(scriptEl);
template = iframe.contentWindow.template;
document.head.appendChild(this.样式元素);
console.log(this.设置字典);
生成标题序号(this.设置字典);
this.eventBus.on("ws-main", this.ws监听器);
}
生成顶栏() {
this.顶栏按钮 = this.addTopBar({
icon: "iconOrderedList",
title: this.i18n.addTopBarIcon,
position: "right",
callback: () => {
this.创建菜单();
},
});
this.顶栏按钮.addEventListener("contextmenu", async () => {
if (!window.isSecureContext) {
return;
}
try {
let 文件数组 = await window.showOpenFilePicker({
types: [
{
description: "配置文件",
accept: {
"application/javascript": [".js"],
"application/json": [".json"],
},
},
],
excludeAcceptAllOption: true,
multiple: true,
});
for await (let 文件句柄 of 文件数组) {
let name = 文件句柄.name;
let file = await 文件句柄.getFile();
await 思源工作空间.writeFile(file, path.join(this.dataPath, name));
}
await this.初始化();
} catch (e) {
console.error(e);
}
});
}
创建菜单() {
const menu = new clientApi.Menu("topBarSample", () => {});
let 配置文件名数组 = Object.getOwnPropertyNames(this.设置字典);
for (let i = 0, len = 配置文件名数组.length; i < len; i++) {
let name = 配置文件名数组[i];
try {
this.添加配置文件选择菜单项(menu, name);
} catch (e) {
console.error(e);
}
}
menu.addSeparator();
menu.addItem({
icon: "iconAdd",
label: this.i18n.添加配置文件,
click: () => {
let Dialog;
Dialog = new clientApi.Dialog({
title: "输入文件名,留空取消",
content: `<div class="fn__flex"><input class="fn__flex-1 b3-text-field b3-filter" placeholder="输文件名,留空取消"></div>`,
width: "400px",
height: "96px",
destroyCallback: async () => {
let name = Dialog.element.querySelector("input").value;
if (name) {
let reg = new RegExp('[\\\\/:*?"<>|]');
if (reg.test(name)) {
return;
}
let 新配置文件路径 = path.join(this.dataPath, name + ".json");
await 思源工作空间.writeFile(
'["","","","","",""]',
新配置文件路径
);
}
await this.初始化();
},
});
},
});
menu.addItem({
icon: "iconRefresh",
label:(this.自动刷新标题?"结束":"开始")+this.i18n.自动刷新标题,
click:()=>{
this.自动刷新标题=!this.自动刷新标题
}
})
menu.open(this.顶栏按钮.getBoundingClientRect());
}
添加配置文件选择菜单项(menu, name) {
if (name == "当前全局配置") {
return;
}
let content = this.设置字典[name];
let element = document.createElement("button");
if (this.设置字典.当前全局配置.name == name) {
element.style.backgroundColor = "var(--b3-card-success-background)";
当前选项按钮 = element;
}
element.setAttribute("class", "b3-menu__item");
element.innerHTML += `<span class="b3-menu__label">${name
.split("/")
.pop()}</span>`;
element.innerHTML +=
'<svg class="b3-menu__icon"><use xlink:href="#iconEdit"></use></svg>';
menu.menu.append(element);
element.addEventListener("click", (e) => {
if (e.target.tagName == "svg" || e.target.tagName == "use") {
this.打开编辑页面(content, name);
return;
}
this.设置字典.当前全局配置.name = name;
this.设置字典.当前全局配置.content = content;
生成标题序号(this.设置字典);
this.saveData(
"lastValues.json",
JSON.stringify({ name: name, content: content })
);
menu.close();
});
}
async 打开编辑页面(content, name) {
const tab = clientApi.openTab({
app: this.app,
custom: {
icon: "iconHeading",
title: name,
data: {
content: content,
name: name,
},
fn: this.customTab,
},
});
console.log(tab);
}
async 覆盖默认设置() {
let jsContent = await (await fetch(this.selfURL + "/实例设置1.js")).text();
let jsonContent = await (
await fetch(this.selfURL + "/实例设置2.json")
).json();
await 思源工作空间.writeFile(
jsContent,
path.join(this.dataPath, "实例设置1.js")
);
if (
!(await 思源工作空间.exists(path.join(this.dataPath, "实例设置2.json")))
) {
await 思源工作空间.writeFile(
JSON.stringify(jsonContent),
path.join(this.dataPath, "实例设置2.json")
);
}
if (
await 思源工作空间.exists(path.join(this.dataPath, "lastValues.json"))
) {
try {
this.设置字典.当前全局配置 = JSON.parse(
await 思源工作空间.readFile(
path.join(this.dataPath, "lastValues.json")
)
);
} catch (e) {
console.error(e);
}
} else {
this.设置字典.当前全局配置 = {};
}
}
async 获取全部设置() {
let 全部配置 = await 思源工作空间.readDir(this.dataPath);
for await (let 配置项 of 全部配置) {
try {
if (!(配置项.isDir || 配置项.name == "lastValues.json")) {
let 配置路径 = path.join(this.dataPath, 配置项.name);
let 配置内容 = 配置项.name.endsWith(".js")
? await 读取js配置(配置路径)
: await 读取json配置(配置路径);
if (!配置内容 instanceof Array) {
console.warn(配置项.name + "没有导出数组");
} else if (配置内容.length < 6) {
配置项.name + "没有配置全部标题序号";
}
this.设置字典[配置项.name.split(".")[0]] = 配置内容;
if (
this.设置字典.当前全局配置 &&
this.设置字典.当前全局配置.name == 配置项.name.split(".")[0]
) {
this.设置字典.当前全局配置.content = 配置内容;
}
}
} catch (e) {
console.error(`配置文件${配置项.name}加载错误`, e);
}
}
}
debounceTimer=null
async ws监听器(detail) {
if(this.自动刷新标题){
this.debounceTimer?clearTimeout(this.debounceTimer):null;
this.debounceTimer = setTimeout(async () => {
await 生成标题序号(that.设置字典);
}, 500); // 300ms为防抖时间可以根据实际情况调整
}
}
}
module.exports = headingIndex;
async function 读取js配置(配置路径) {
let jsContent = (await 思源工作空间.readFile(配置路径)).toString();
let blob = new Blob(
[数字转中文.toString() + "\n" + jsContent + '\n//# sourceURL="' + 配置路径],
{ type: "application/javascript" }
);
let moduleURL = URL.createObjectURL(blob);
return (await import(moduleURL)).default;
}
async function 读取json配置(配置路径) {
let jsonContent = await 思源工作空间.readFile(配置路径);
return JSON.parse(jsonContent);
}
let 已提示块 = {};
async function 生成标题序号(序号设置字典, 文档id) {
if (文档id) {
await 生成文档内标题序号(文档id, 序号设置字典);
}
let 文档面包屑数组 = document.querySelectorAll(
".protyle-breadcrumb__bar span:first-child[data-node-id]"
);
文档面包屑数组.forEach(async (文档面包屑元素) => {
let 文档id = 文档面包屑元素.getAttribute("data-node-id");
try {
let 预取内容 = await 核心api.getDoc({ id: 文档id, size: 1 });
if (已提示块[文档id]) {
return;
}
if (预取内容.blockCount > 1024) {
let 文档信息 = await 核心api.getDocInfo({ id: 文档id });
核心api.pushMsg({
msg: `${文档信息.name}内块数量为${预取内容.blockCount},超过阈值,请手动生成`,
});
已提示块[文档id] = true;
return;
}
await 生成文档内标题序号(文档id, 序号设置字典);
} catch (e) {
console.warn(e);
if (当前选项按钮 && 当前选项按钮.parentElement) {
当前选项按钮.style.backgroundColor = "var(--b3-card-error-background)";
当前选项按钮.parentElement.insertAdjacentHTML(
"beforeend",
Lute.EscapeHTMLStr(e.toString())
);
}
}
});
}
async function 生成文档内标题序号(文档id, 序号设置字典, 写入序号) {
let 文档内容 = await 核心api.getDoc({ id: 文档id, size: 102400 });
let 文档信息 = await 核心api.getDocInfo({ id: 文档id });
/*if(.content.lenth>100000){
return
}*/
let 当前序号设置 = 序号设置字典.当前全局配置.content;
if (文档信息.ial && 文档信息.ial["custom-index-scheme"] === "null") {
return;
}
if (文档信息.ial && 文档信息.ial["custom-index-scheme"]) {
当前序号设置 =
序号设置字典[文档信息.ial["custom-index-scheme"]] || 当前序号设置;
}
if (!当前序号设置) {
return;
}
let parser = new DOMParser();
let 临时元素 = parser.parseFromString(文档内容.content, "text/html").body;
//console.log(临时元素)
// 临时元素.innerHTML = 文档内容.content;
let 标题元素数组 = 临时元素.querySelectorAll(
'[data-type="NodeHeading"]:not( [data-type="NodeBlockQueryEmbed"] div)'
);
let 计数器 = [0, 0, 0, 0, 0, 0];
let 上一个标题级别 = 1;
for (let i = 0; i < 标题元素数组.length; ++i) {
let 标题元素 = 标题元素数组[i];
if (!标题元素数组[i].querySelector("[contenteditable]")) {
return;
}
if (!标题元素数组[i].querySelector("[contenteditable]").innerText) {
let 标题id = 标题元素数组[i].getAttribute("data-node-id");
document
.querySelectorAll(`.protyle-wysiwyg div[data-node-id='${标题id}']`)
.forEach((一级标题元素) => {
一级标题元素
.querySelector("[contenteditable]")
.style.removeProperty("--custom-index");
});
return;
}
let 当前标题级别 = parseInt(
标题元素数组[i].getAttribute("data-subtype").replace("h", "")
);
if (当前标题级别 <= 上一个标题级别) {
for (let j = 0; j < 计数器.length; ++j) {
if (j + 1 > 当前标题级别) {
计数器[j] = 0;
}
}
}
计数器[当前标题级别 - 1] += 1;
let 标题id = 标题元素数组[i].getAttribute("data-node-id");
if (!当前序号设置[当前标题级别 - 1]) {
document
.querySelectorAll(`.protyle-wysiwyg div[data-node-id='${标题id}']`)
.forEach(async (标题元素) => {
let 内容元素 = 标题元素.querySelector("[contenteditable]");
内容元素.setAttribute("style", `--custom-index:""`);
});
document
.querySelectorAll(`.sy__outline [data-node-id=""]`)
.forEach(async (大纲项目) => {
大纲项目.setAttribute("style", `--custom-index:""`);
});
}
if (当前序号设置[当前标题级别 - 1]) {
let 当前序号;
if (当前序号设置[当前标题级别 - 1] instanceof Function) {
当前序号 = 当前序号设置[当前标题级别 - 1](计数器[当前标题级别 - 1]);
} else {
function h(级别) {
let num = 计数器[级别 - 1];
let obj = () => {
return num;
};
obj.num = num;
obj.ch = 数字转中文(num);
obj.roman = numToRoman(num);
obj.en = numToEnglish(num);
obj.CH = 数字转中文(num, true);
obj.abc = 数字转字母(num, false);
obj.ABC = 数字转字母(num, true);
obj.enth = numToEnglish(num, false);
obj.ru = toRussian(num);
obj.toString = () => {
return Obj.num;
};
document.querySelectorAll("script").forEach((scriptEl) => {
try {
let indexFormatters;
if (
scriptEl.indexFormatters &&
scriptEl.indexFormatters instanceof Array
) {
indexFormatters = scriptEl.indexFormatters;
}
if (
scriptEl.序号格式化函数组 &&
scriptEl.序号格式化函数组 instanceof Array
) {
indexFormatters = scriptEl.序号格式化函数组;
}
if(!indexFormatters){
return
}
indexFormatters.forEach((fn) => {
if (
fn.formatter instanceof Function &&
fn.name
) {
obj[fn.name] = fn.formatter(num,obj,标题元素.dataset.nodeId);
}
if (
fn.格式化函数 instanceof Function &&
fn.名称
) {
obj[fn.name] = fn.格式化函数(num,obj,标题元素.dataset.nodeId);
}
});
} catch (e) {
console.warn("标题序号定义错误", scriptEl, e);
}
});
return obj;
}
template.defaults.rules[1].test =
/{([@#]?)[ \t]*(\/?)([\w\W]*?)[ \t]*}/;
let string = 当前序号设置[当前标题级别 - 1];
let render = template.compile(string);
当前序号 = render({
h1: h(1),
h2: h(2),
h3: h(3),
h4: h(4),
h5: h(5),
h6: h(6),
});
}
if (写入序号) {
let 旧标题序号元素 = 标题元素.querySelector(
'span[style~="--custom-index:true"]'
);
if (旧标题序号元素) {
旧标题序号元素.remove();
}
标题元素
.querySelector('[contenteditable="true"]')
.insertAdjacentHTML(
"afterBegin",
`<span style="--custom-index:true">${当前序号}</span>`
);
await 核心api.updateBlock({
dataType: "dom",
data: 标题元素.outerHTML,
id: 标题id,
});
document
.querySelectorAll(`.protyle-wysiwyg div[data-node-id='${标题id}']`)
.forEach(async (标题元素) => {
let 内容元素 = 标题元素.querySelector("[contenteditable]");
内容元素.setAttribute("style", ``);
});
document
.querySelectorAll(`.sy__outline [data-node-id="${标题id}"]`)
.forEach(async (大纲项目) => {
大纲项目.setAttribute("style", ``);
});
} else {
document
.querySelectorAll(`.protyle-wysiwyg div[data-node-id='${标题id}']`)
.forEach(async (标题元素) => {
let 内容元素 = 标题元素.querySelector("[contenteditable]");
内容元素.setAttribute("style", `--custom-index:"${当前序号}"`);
});
document
.querySelectorAll(`.sy__outline [data-node-id="${标题id}"]`)
.forEach(async (大纲项目) => {
大纲项目.setAttribute("style", `--custom-index:"${当前序号}"`);
});
}
上一个标题级别 = 当前标题级别 + 0;
}
}
}
//作者houyhea
//链接https://juejin.cn/post/6844903473255809038
//来源:稀土掘金
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
function 数字转中文(digit, 大写) {
digit = typeof digit === "number" ? String(digit) : digit;
let zh = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
let unit = ["千", "百", "十", ""];
if (大写) {
zh = ["零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"];
unit = ["仟", "佰", "拾", ""];
}
const quot = [
"万",
"亿",
"兆",
"京",
"垓",
"秭",
"穰",
"沟",
"涧",
"正",
"载",
"极",
"恒河沙",
"阿僧祗",
"那由他",
"不可思议",
"无量",
"大数",
];
let breakLen = Math.ceil(digit.length / 4);
let notBreakSegment = digit.length % 4 || 4;
let segment;
let zeroFlag = [],
allZeroFlag = [];
let result = "";
while (breakLen > 0) {
if (!result) {
// 第一次执行
segment = digit.slice(0, notBreakSegment);
let segmentLen = segment.length;
for (let i = 0; i < segmentLen; i++) {
if (segment[i] != 0) {
if (zeroFlag.length > 0) {
result += "零" + zh[segment[i]] + unit[4 - segmentLen + i];
// 判断是否需要加上 quot 单位
if (i === segmentLen - 1 && breakLen > 1) {
result += quot[breakLen - 2];
}
zeroFlag.length = 0;
} else {
result += zh[segment[i]] + unit[4 - segmentLen + i];
if (i === segmentLen - 1 && breakLen > 1) {
result += quot[breakLen - 2];
}
}
} else {
// 处理为 0 的情形
if (segmentLen == 1) {
result += zh[segment[i]];
break;
}
zeroFlag.push(segment[i]);
continue;
}
}
} else {
segment = digit.slice(notBreakSegment, notBreakSegment + 4);
notBreakSegment += 4;
for (let j = 0; j < segment.length; j++) {
if (segment[j] != 0) {
if (zeroFlag.length > 0) {
// 第一次执行zeroFlag长度不为0说明上一个分区最后有0待处理
if (j === 0) {
result += quot[breakLen - 1] + zh[segment[j]] + unit[j];
} else {
result += "零" + zh[segment[j]] + unit[j];
}
zeroFlag.length = 0;
} else {
result += zh[segment[j]] + unit[j];
}
// 判断是否需要加上 quot 单位
if (j === segment.length - 1 && breakLen > 1) {
result += quot[breakLen - 2];
}
} else {
// 第一次执行如果zeroFlag长度不为0, 且上一划分不全为0
if (j === 0 && zeroFlag.length > 0 && allZeroFlag.length === 0) {
result += quot[breakLen - 1];
zeroFlag.length = 0;
zeroFlag.push(segment[j]);
} else if (allZeroFlag.length > 0) {
// 执行到最后
if (breakLen == 1) {
result += "";
} else {
zeroFlag.length = 0;
}
} else {
zeroFlag.push(segment[j]);
}
if (
j === segment.length - 1 &&
zeroFlag.length === 4 &&
breakLen !== 1
) {
// 如果执行到末尾
if (breakLen === 1) {
allZeroFlag.length = 0;
zeroFlag.length = 0;
result += quot[breakLen - 1];
} else {
allZeroFlag.push(segment[j]);
}
}
continue;
}
}
--breakLen;
}
return result;
}
}
//转换为罗马数字
function numToRoman(num) {
const romanNumMap = [
["I", "IV", "V", "IX"],
["X", "XL", "L", "XC"],
["C", "CD", "D", "CM"],
["M"]
];
let romanNum = "";
let digits = num.toString().split('').reverse();
for (let i = 0; i < digits.length; i++) {
let digit = parseInt(digits[i]);
if (digit <= 3) {
romanNum = romanNumMap[i][0].repeat(digit) + romanNum;
} else if (digit === 4) {
romanNum = romanNumMap[i][1] + romanNum;
} else if (digit <= 8) {
romanNum = romanNumMap[i][2] + romanNumMap[i][0].repeat(digit - 5) + romanNum;
} else if (digit === 9) {
romanNum = romanNumMap[i][3] + romanNum;
}
}
return romanNum;
}
/*function numToRoman(num) {
const romanNumMap = {
0: "",
1: "I",
2: "II",
3: "III",
4: "IV",
5: "V",
6: "VI",
7: "VII",
8: "VIII",
9: "IX",
};
// 拆分数字字符串
let numStr = num.toString().split("");
let romanNum = "";
for (let i = 0; i < numStr.length; i++) {
let digit = numStr[i];
let nextDigit = numStr[i + 1];
// 特殊情况4和9处理
if (+digit === 4 && +nextDigit === 1) {
romanNum += "IV";
i++;
} else if (+digit === 9 && +nextDigit === 1) {
romanNum += "IX";
i++;
} else {
// 如果大于5,拆分处理
if (+digit > 5) {
romanNum += romanNumMap[5];
for (let j = 1; j < +digit - 5; j++) {
romanNum += romanNumMap[1];
}
} else {
romanNum += romanNumMap[+digit];
}
}
}
return romanNum;
}*/
const englishNumMap = {
0: "zero",
1: "one",
2: "two",
3: "three",
4: "four",
5: "five",
6: "six",
7: "seven",
8: "eight",
9: "nine",
10: "ten",
11: "eleven",
12: "twelve",
13: "thirteen",
14: "fourteen",
15: "fifteen",
16: "sixteen",
17: "seventeen",
18: "eighteen",
19: "nineteen",
20: "twenty",
30: "thirty",
40: "forty",
50: "fifty",
60: "sixty",
70: "seventy",
80: "eighty",
90: "ninety",
};
function numToEnglish(num) {
let englishNum = "";
if (num === 0) return englishNumMap[0];
if (num < 20) return englishNumMap[num]; // 1-19
if (num < 100) {
// 20-99
englishNum += englishNumMap[Math.floor(num / 10) * 10];
englishNum += num % 10 === 0 ? "" : " " + numToEnglish(num % 10);
} else if (num < 1000) {
// 100-999
englishNum += englishNumMap[Math.floor(num / 100)] + " hundred";
if (num % 100 > 0) englishNum += " and " + numToEnglish(num % 100);
} else if (num < 1e6) {
// 1000-999999
englishNum += numToEnglish(Math.floor(num / 1000)) + " thousand";
if (num % 1000 > 0) englishNum += " " + numToEnglish(num % 1000);
} else if (num < 1e9) {
// 1e6-999999999
englishNum += numToEnglish(Math.floor(num / 1e6)) + " million";
if (num % 1e6 > 0) englishNum += " " + numToEnglish(num % 1e6);
}
return englishNum.trim();
}
function 数字转字母(num, upper) {
if (num <= 26) {
return upper
? String.fromCharCode(64 + num)
: String.fromCharCode(64 + num).toLowerCase();
} else {
return num;
}
}
let russianNames = [
"нуль",
"один",
"два",
"три",
"четыре",
"пять",
"шесть",
"семь",
"восемь",
"девять",
"десять",
"одиннадцать",
"двенадцать",
"тринадцать",
"четырнадцать",
"пятнадцать",
"шестнадцать",
"семнадцать",
"восемнадцать",
"девятнадцать",
"двадцать",
"двадцать один",
"двадцать два",
"двадцать три",
"двадцать четыре",
"двадцать пять",
"двадцать шесть",
"двадцать семь",
"двадцать восемь",
"двадцать девять",
"тридцать",
"сорок",
"пятьдесят",
"шестьдесят",
"семьдесят",
"восемьдесят",
"девяносто",
];
function toRussian(n) {
if (n < 0) return "минус " + toRussian(-n);
if (n < 20) return russianNames[n];
let hundreds = Math.floor(n / 100);
let tens = Math.floor((n % 100) / 10);
let ones = n % 10;
let result = "";
if (hundreds) result += russianNames[hundreds] + " сто ";
if (tens || ones) {
result += russianNames[tens * 10];
if (ones) result += " " + russianNames[ones];
}
return result;
}