#!/usr/bin/env node const fs = require('node:fs'); const path = require('node:path'); const LANGUAGES_DIR = path.join(__dirname, 'languages'); const PROVIDERS_PATH = path.join(__dirname, 'providers'); const DEFAULT_LANG = 'en'; const OUTPUT_DIR = __dirname; const REPO_RAW = 'https://github.com/paulmillr/encrypted-dns/raw/master'; const REGIONS = { US: 'πŸ‡ΊπŸ‡Έ', CN: 'πŸ‡¨πŸ‡³', RU: 'πŸ‡·πŸ‡Ί', NL: 'πŸ‡³πŸ‡±', DE: 'πŸ‡©πŸ‡ͺ', SG: 'πŸ‡ΈπŸ‡¬', CA: 'πŸ‡¨πŸ‡¦', FR: 'πŸ‡«πŸ‡·', CH: 'πŸ‡¨πŸ‡­', SE: 'πŸ‡ΈπŸ‡ͺ', CZ: 'πŸ‡¨πŸ‡Ώ', }; const providerFile = (p, https, signed) => { const postfix = (p, https) => { if (https) { if (p.doh && p.doh !== true) return `doh${p.doh}`; if (p.doh) return 'doh'; return 'https'; } else { if (p.doh && p.doh !== true) return `dot${p.doh}`; if (p.doh) return 'dot'; return 'tls'; } }; const name = p.name || p.id; return `${signed ? 'signed' : 'profiles'}/${name}-${postfix(p, https)}.mobileconfig`; }; const languages = fs .readdirSync(LANGUAGES_DIR) .filter((f) => f.endsWith('.json')) .sort() .map((f) => { const p = path.join(LANGUAGES_DIR, f); const data = JSON.parse(fs.readFileSync(p, 'utf8')); return { code: data.code, name: data.name, mdFile: p.replace('.json', '.md'), jsonFile: p, data: data, }; }); const providers = fs .readdirSync(PROVIDERS_PATH) .sort() .map((i) => JSON.parse(fs.readFileSync(`${PROVIDERS_PATH}/${i}`))) .map((i) => { const unsigned = { https: !!i.https || fs.existsSync(providerFile(i, true)), tls: !!i.tls || fs.existsSync(providerFile(i, false)), }; const signed = { https: unsigned.https && fs.existsSync(providerFile(i, true, true)), tls: unsigned.tls && fs.existsSync(providerFile(i, false, true)), }; return { ...i, formats: { unsigned, signed } }; }); const FULLWIDTH_PATTERN = /[\u1100-\u115F\u2329\u232A\u2E80-\u303E\u3040-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6]/u; function chrWidth(str) { let width = 0; for (const char of str) width += FULLWIDTH_PATTERN.test(char) || REGIONS[char] ? 2 : 1; return width; } const padEnd = (s, len, chr) => `${s}${chr.repeat(Math.max(0, len - chrWidth(s)))}`; const genTable = (rows) => { // first row is header const widths = rows[0].map((i) => 0); // Collect cell widths for (const r of rows) { for (let i = 0; i < r.length; i++) widths[i] = Math.max(widths[i], chrWidth(r[i])); } let table = ''; rows.forEach((r, i) => { const cells = r.map((c, j) => padEnd(c, widths[j], ' ')).join(' | '); table += `| ${cells} |\n`; if (i === 0) table += `| ${r.map((c, j) => padEnd('', widths[j], '-')).join(' | ')} |\n`; }); return table; }; const TAGS = { // Language selection header LANGUAGES: (currentLang) => { return languages .map((lang) => { if (lang.code === currentLang.code) return lang.name; return `[${lang.name}](https://github.com/paulmillr/encrypted-dns/${ lang.code === DEFAULT_LANG ? '' : `blob/master/README.${lang.code}.md` })`; }) .join(' | '); }, PROVIDERS_TABLE: (currentLang) => { const rows = [ // header [ currentLang.data.table_columns.name, currentLang.data.table_columns.region, currentLang.data.table_columns.censorship, currentLang.data.table_columns.notes, currentLang.data.table_columns.install_signed, currentLang.data.table_columns.install_unsigned, ], ]; const sorted = Array.from(providers).sort((a, b) => a.id.localeCompare(b.id)); for (const provider of sorted) { const name = provider.names[currentLang.code] || provider.names[DEFAULT_LANG]; const note = provider.notes[currentLang.code] || provider.notes[DEFAULT_LANG]; const censorship = provider.censorship ? currentLang.data.yes : currentLang.data.no; const regionEmoji = (Array.isArray(provider.region) ? provider.region : [provider.region]) .map((r) => REGIONS[r]) .join(' '); const unsignedLinks = []; if (provider.formats.unsigned && provider.formats.unsigned.https) unsignedLinks.push(`[HTTPS][${provider.profile}-https]`); if (provider.formats.unsigned && provider.formats.unsigned.tls) unsignedLinks.push(`[TLS][${provider.profile}-tls]`); const signedLinks = []; if (provider.formats.signed && provider.formats.signed.https) signedLinks.push(`[HTTPS][${provider.profile}-https-signed]`); if (provider.formats.signed && provider.formats.signed.tls) signedLinks.push(`[TLS][${provider.profile}-tls-signed]`); rows.push([ `[${name}][${provider.id}]`, regionEmoji, censorship, note, signedLinks.join(', '), unsignedLinks.join(', '), ]); } return genTable(rows).trim(); }, PROVIDERS_LINKS: (currentLang) => { let res = ''; const addLink = (p, https, signed) => { const file = providerFile(p, https, signed); if (!fs.existsSync(`./${file}`)) throw new Error('missing: ' + file); res += `[${p.profile}-${https ? 'https' : 'tls'}${ signed ? '-signed' : '' }]: ${REPO_RAW}/${file}\n`; }; for (const p of providers) { if (p.website) res += `[${p.id}]: ${p.website}\n`; if (p.formats.unsigned.https) addLink(p, true); if (p.formats.unsigned.tls) addLink(p, false); } // signed for (const p of providers) { if (p.formats.signed.https) addLink(p, true, true); if (p.formats.signed.tls) addLink(p, false, true); } return res; }, }; function processTemplate(templateContent, lang) { let content = templateContent; for (const [tag, handler] of Object.entries(TAGS)) { const tagPattern = new RegExp(`<%${tag}%>`, 'g'); if (content.match(tagPattern)) content = content.replace(tagPattern, handler(lang)); } return content; } function generateReadmes() { for (const lang of languages) { if (!fs.existsSync(lang.mdFile)) throw new Error(`Template file not found: ${lang.mdFile}`); const tpl = fs.readFileSync(lang.mdFile, 'utf8'); const processed = processTemplate(tpl, lang); const out = lang.code === DEFAULT_LANG ? 'README.md' : `README.${lang.code}.md`; fs.writeFileSync(path.join(OUTPUT_DIR, out), processed, 'utf8'); console.log(`Generated ${out}`); } } function generateSingle(x) { return ` PayloadContent DNSSettings DNSProtocol ${x.DNSProtocol} ServerAddresses ${x.ServerAddresses.map((i) => `\t\t\t\t\t${i}`).join('\n')} ${!x.ServerURLOrName.startsWith('https://') ? 'ServerName' : 'ServerURL'} ${x.ServerURLOrName} PayloadDescription Configures device to use ${x.name} PayloadDisplayName ${x.PayloadDisplayName} PayloadIdentifier ${x.PayloadIdentifier} PayloadType com.apple.dnsSettings.managed PayloadUUID ${x.PayloadUUID} PayloadVersion 1 ProhibitDisablement PayloadDescription Adds the ${x.fullName} to Big Sur and iOS 14 based systems PayloadDisplayName ${x.topName} PayloadIdentifier com.paulmillr.apple-dns PayloadRemovalDisallowed PayloadScope System PayloadType Configuration PayloadUUID ${x.TopPayloadUUID} PayloadVersion 1 `; } function generateConfigs() { function generate(file, parsed) { if (!parsed) return; fs.writeFileSync(file, generateSingle(parsed)); console.log(`Generated ${file}`); } for (const p of providers) { if (p.formats.unsigned.https) generate(providerFile(p, true), p.https); if (p.formats.unsigned.tls) generate(providerFile(p, false), p.tls); } } // Small utility to rewrite config structure function patchConfigs() { for (const f of fs.readdirSync(`./providers/`)) { const path = `./providers/${f}`; const json = JSON.parse(fs.readFileSync(path, 'utf8')); fs.writeFileSync(path, JSON.stringify(json, null, 4)); } } function main() { //patchConfigs(); generateReadmes(); generateConfigs(); } main();