Files
server-configs/siyuan/data/plugins/headingIndex/index.js
2026-02-13 22:24:27 +08:00

976 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}