UNPKG

@cto.af/ucd

Version:

Download, cache, and parse files from the Unicode Character Database (UCD).

2 lines (1 loc) 2.84 kB
import*as o from"node:fs/promises";import*as d from"node:path";import{getLog as D}from"@cto.af/log";import{errCode as P,isCI as F}from"@cto.af/utils";import{Buffer as C}from"node:buffer";import{fileURLToPath as y}from"node:url";import{parse as b}from"./ucd.js";const x="https://www.unicode.org/Public/UCD/latest/ucd/",g="---000---",v={cacheDir:process.cwd(),alwaysParse:!1,checkinCI:!1,prefix:x,verbose:!1};function p(s){return s.status===200}function R(s){if(typeof s=="string")return d.resolve(s);if(s instanceof URL)return d.resolve(y(s));if(C.isBuffer(s))return d.resolve(s.toString("utf8"));if(s==null)return process.cwd();throw new TypeError(`Invalid cacheDir: ${s}`)}class m{#e;#t;constructor(e={}){this.#e={...v,...e,cacheDir:R(e.cacheDir)},this.#t=D({logLevel:this.#e.verbose?1:0})}static async create(e={}){return new m(e).init()}async init(){try{if(!(await o.stat(this.#e.cacheDir)).isDirectory())throw new Error(`Cache directory not directory: "${this.#e.cacheDir}"`)}catch(e){if(P(e,"ENOENT"))await o.mkdir(this.#e.cacheDir,{recursive:!0});else throw e}return await o.access(this.#e.cacheDir),this}rmDir(){return o.rm(this.#e.cacheDir,{recursive:!0})}async fetchUCDversion(e){const t=await this.#i("ReadMe.txt",e);if(!p(t))throw new Error("Version not changed");const{text:i,lastModified:r,etag:n}=t,a=i.match(/^# Date: (?<date>\d+-\d+-\d+)/m),l=i.match(/Version (?<version>\d+\.\d+\.\d+) of/i);if(!a?.groups||!l?.groups)throw new Error("Invalid ReadMe");return{date:new Date(a.groups.date),version:l.groups.version,lastModified:r,etag:n}}async parse(e,t){const i=await this.#i(e,t);return p(i)&&(i.parsed=b(i.text)),i}async#i(e,t){let i="",r=500,n=g,a=new Date().toUTCString();if(F(t)&&!this.#e.checkinCI)r=304;else{const h=new URL(e,this.#e.prefix),u={},f=[];if(t?.lastModified&&f.push(["if-modified-since",t.lastModified]),t?.etag){const w=t.etag.replace(/-gzip"$/,'"');f.push(["if-none-match",w])}f.length&&(u.headers=f),this.#t.debug('Checking "%s" with headers: %o',h,u.headers??{});const c=await fetch(h,u);switch({status:r}=c,n=c.headers.get("etag")??n,a=c.headers.get("last-modified")??a,c.status){case 200:i=await c.text(),await this.#o(e,i);break;case 304:break;default:throw this.#t.error("Unexpected HTTP status: %d",r),new Error(`HTTP Status ${r}`)}}if(r===304)if(this.#e.alwaysParse)this.#t.debug("Switching 304 to 200 because alwaysParse"),i=await this.#r(e);else return{etag:t?.etag??g,lastModified:t?.lastModified??new Date().toUTCString(),status:r};return{etag:n,lastModified:a,status:200,text:i}}#s(e){return d.join(this.#e.cacheDir,e)}#r(e){const t=this.#s(e);return this.#t.debug('Reading "%s"',t),o.readFile(t,"utf8")}async#o(e,t){const i=this.#s(e);this.#t.debug('Writing "%s"',i);const r=d.dirname(i);return await o.mkdir(r,{recursive:!0}),o.writeFile(i,t,"utf8")}}export{m as UCD,p as isSuccess};