"use strict"; const cmn = require("../$common.js"); const fs = require("fs"); const path = require("path"); const https = require("https"); const fCODE = 0; const fNAME = 1; const fCATEGORY = 2; // combining class, bidirectional category, decomposition mapping const fDECDIGIT = 6; const fDIGIT = 7; const fNUMERIC = 8; const fMIRRORED = 9; const fOLDNAME = 10; const fCOMMENT = 11; const fUPPERCASE = 12; const fLOWERCASE = 13; const fTITLECASE = 14; // db: Map(int => [Field]) (NAME is in uppercase) let db = new Map(); // ranges: [{name: String, first: int, last: int}] let ranges = []; // aliases: [{name: String, code: int}] let aliases = []; function importDatabase(csv) { // clear the database if necessary db = new Map(); ranges = []; let currentRange = null; let cursor = 0, endidx; while ((endidx = csv.indexOf("\n", cursor)) != -1) { const row = csv.slice(cursor, endidx).split(";"); cursor = endidx + 1; const code = parseInt(row[fCODE], 16); const m = row[fNAME].match(/^<(.*), (First|Last)>$/) if (m != null) { if (m[2] == "First") { row[fNAME] = "<" + m[1] + ">"; currentRange = {name: m[1], first: code, last: null, row: row}; } else if (currentRange != null && m[1] == currentRange.name) { currentRange.last = code; ranges.push(currentRange); currentRange = null; } } else { row[fNAME] = row[fNAME].toUpperCase(); db.set(code, row); } } } function importAliases(lines) { aliases = []; for (const line of lines.split("\n")) { const idx = line.indexOf(" "); if (idx == -1) continue; const num = parseInt(line.slice(0, idx)); const descr = line.slice(idx + 1).toUpperCase(); aliases.push({name: descr, code: num}); } } function lookupCode(codepoint) { for (const range of ranges) { if (range.first <= codepoint && codepoint <= range.last) { const row = range.row.slice(); row[fCODE] = codepoint.toString(16).toUpperCase(); return row; } } return db.get(codepoint); } function searchDescription(text) { text = text.toUpperCase(); const result = []; for (const row of db.values()) { if (row[fNAME].includes(text)) { result.push(row); if (result.length >= 500) return result; } } for (const row of aliases) { if (row.name.includes(text)) { result.push(db.get(row.code)); if (result.length >= 500) return result; } } return result; } function recogniseIndices(text) { const results = []; // A: 1234 (decimal) // B: 0x34ab / U+34ab (hexadecimal) // C: Ӓ (xml-style decimal) // D: 㒫 (xml-style hexadecimal) // within word boundaries. // AAAAAA BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB CCCCCCCCCCC DDDDDDDDDDDDDDDDDDDDD const regex = /\b([0-9]+|(?:0[Xx]|[Uu]\+)([0-9a-fA-F]+)|&#([0-9]+);|&#[Xx]([0-9a-fA-F]+);)\b/g; const matches = text.match(regex); if (matches == null) return []; for (const m of matches) { let code = -1; if ("Xx+".indexOf(m[1]) != -1) { // hexadecimal code = parseInt(m.slice(2), 16); } else if (m[0] == "&" && "Xx".indexOf(m[2]) != -1) { // xml-style hexadecimal code = parseInt(m.slice(3, -1), 16); } else if (m[0] == "&") { // xml-style decimal code = parseInt(m.slice(2, -1), 10); } else { // decimal code = parseInt(m, 10); } const row = lookupCode(code); if (row != null) results.push(row); } return results; } module.exports = function (app, io, moddir) { const dataFilePath = path.join(moddir, "UnicodeData.txt"); if (fs.existsSync(dataFilePath)) { importDatabase(fs.readFileSync(dataFilePath).toString()); } else { throw new Error("UnicodeData.txt doesn't exist! Be sure to run ./install_prepare.sh"); } importAliases(fs.readFileSync(path.join(moddir, "aliases.txt")).toString()); app.get("/unicode", (req, res) => { res.sendFile(path.join(moddir, "index.html")); }); app.get("/unicode/lookup/:query", (req, res) => { const codepoints = []; let notfound = ""; for (let codepoint of req.params.query) { codepoint = codepoint.codePointAt(0); const result = lookupCode(codepoint); if (result != null) codepoints.push(result); else notfound += String.fromCodePoint(codepoint); } res.json({ indices: recogniseIndices(req.params.query), codepoints: codepoints, notfound: notfound, search: searchDescription(req.params.query), }); }); };