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", `` ); 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", ` ` ); 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: `
`, 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 += `${name .split("/") .pop()}`; element.innerHTML += ''; 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", `${当前序号}` ); 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; }