commit a7dedf56408225d5ef6b7baac8f583e0a4f123f1 Author: Sofía Aritz Date: Mon May 15 20:17:18 2023 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b580fd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Force reloading of git-based modules +yarn.lock + +# Scripts folder +_scripts/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3541fd1 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# GFonts Mirror Interface +I need a better name :p + +This utility helps create CSS files to load your fonts from a mirror of Google Fonts from Google Font links. + +## Usage + +1. Go to [gfonts.sofiaritz.com](https://gfonts.sofiaritz.com/). +2. Enter your Mirror URL (an Internet-accesible clone of the [Google Fonts repo](https://github.com/google/fonts)). + * If your site has low-traffic, feel free to use [my mirror](https://cdn.sofiaritz.com/fonts) :) +3. Click the _Transform_ button and the CSS will be generated for you! + * You can use the _Copy code_ button, but sometimes it fails, so keep that in mind! + +## Self-hosting + +### Google Fonts Mirror + +Creating a Google Fonts mirror is an easy task. You need the following: +1. A reliable server (e.g. a VPS). +2. [git](https://git-scm.com/). +3. A way to host the files ([Caddy](https://caddyserver.com/), [NGINX](https://www.nginx.com/), [Apache HTTP Server](https://httpd.apache.org/), etc.) +4. (Technically this is not a requirement, but you should use one anyway) A domain. + +Then follow the following steps: +1. Run `git clone https://github.com/google/fonts --branch main --single-branch --depth 1 [folder]` + * `[folder]` should be replaced with the folder where you will save the mirror. + * This is just a [shallow clone](https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---depthltdepthgt) + of the repo. You can just `git clone` the repo, but it's not recommended due to disk and bandwidth usage. +2. Serve the files somewhere. + * [Caddy](https://caddyserver.com/docs/quick-starts/static-files) + * [NGINX](https://docs.nginx.com/nginx/admin-guide/web-server/serving-static-content/) + * [Apache](https://askubuntu.com/questions/556858/how-to-set-up-a-simple-file-server) +3. Check that the mirror works by accessing the `[root]/README.md` + * `[root]` should be replaced with your domain + the path where it's served (for example, [cdn.sofiaritz.com/fonts](https://cdn.sofiaritz.com/fonts)). + * If everything is working, a README.md file like [this one](https://github.com/google/fonts/blob/main/README.md) should be downloaded. +4. (Optional) Create a `index.html` file. + * You can create a `index.html` file with something you want to tell about the mirror at `[folder]`. + * For example, check [cdn.sofiaritz.com/fonts](https://cdn.sofiaritz.com/fonts). + * I use that page to teach the users about the dangers of services like Google Fonts, how I handle personal data + and my contact information. + +### GFonts Interface + +1. Run `yarn build`. +2. Follow step 2 of [Self-hosting § Google Fonts Mirror](#google-fonts-mirror) with the `dist/` folder. + +## Contributing + +Feel free to contribute to this project! Create an account on my git server, open an issue or a PR, and I'll make sure to +review it as soon as possible :) + +You can also send me the [diff](https://git-scm.com/docs/git-diff) +or use [git-send-email](https://git-scm.com/docs/git-send-email). + +### Notes +* Make sure to delete your `yarn.lock` when a new [pb-parser](https://git.sofiaritz.com/sofia/pb-parser/src/branch/main) +commit is pushed. + +## Decentralization + +Right now the only instance of this interface is [mine](https://gfonts.sofiaritz.com/), and the only Google Fonts mirror +that I know is mine. + +I try to maintain everything and help everyone to achieve decentralization, but at the end of the day I'm just a random +woman. + +**Do you have a Google Fonts mirror?** Please, [contact me](https://sofiaritz.com/en/contact)! + +**Do you have another instance of this interface?** Please, [contact me](https://sofiaritz.com/en/contact)! diff --git a/index.html b/index.html new file mode 100644 index 0000000..04534b4 --- /dev/null +++ b/index.html @@ -0,0 +1,20 @@ + + + + + Google Fonts Mirror Interface + + +
+

Google Fonts Mirror Interface

+ + Transform Google Fonts links to a CSS from a Google Fonts binary mirror. + Created by Sofía Aritz. + +
+
+
+ +
+ + diff --git a/package.json b/package.json new file mode 100644 index 0000000..e153ab1 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "google-fonts-mirror-interface", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json", + "deploy": "cd _scripts && deploy.sh" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.0.3", + "@tsconfig/svelte": "^4.0.1", + "svelte": "^3.57.0", + "svelte-check": "^2.10.3", + "tslib": "^2.5.0", + "typescript": "^5.0.2", + "vite": "^4.3.2" + }, + "dependencies": { + "pb-parser": "git+https://git.sofiaritz.com/sofia/pb-parser" + } +} diff --git a/src/App.svelte b/src/App.svelte new file mode 100644 index 0000000..b1b7a51 --- /dev/null +++ b/src/App.svelte @@ -0,0 +1,34 @@ + + +

Mirror URL

+ + +

Google Fonts URL

+ + + + + +{#await output} +{:then v} + {#if v != null} + {v} + {/if} +{:catch e} +
Error :( {e}
+{/await} diff --git a/src/lib/PreCopy.svelte b/src/lib/PreCopy.svelte new file mode 100644 index 0000000..735e5cc --- /dev/null +++ b/src/lib/PreCopy.svelte @@ -0,0 +1,17 @@ + + +
+
+
+
+ + diff --git a/src/lib/css/app.css b/src/lib/css/app.css new file mode 100644 index 0000000..ba310d2 --- /dev/null +++ b/src/lib/css/app.css @@ -0,0 +1,83 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + min-height: 100%; + color: rgba(255, 255, 255, 0.9); + + background-color: #2f1549; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; +} + +a { + color: #e74cac; +} + +pre { + font-family: "JetBrains Mono", monospace; + font-size: .9em; + background: rgba(0, 0, 0, 0.1); + padding: .5rem; + overflow-x: scroll; +} + +button { + color: white; + padding: 3px; + margin: 3px; + + transition: all 150ms; + background-color: #dc3f99; + border: solid 3px; + border-color: #f38cc2 #dc158d #dc158d #f38cc2; +} + +button:hover { + cursor: pointer; + border-color: #d76d9c #ff0088 #ff0088 #d76d9c; +} + +input[type=url] { + width: 100%; + padding: 3px; + margin: 3px; +} + +.page-container { + display: flex; + justify-content: center; +} + +main { + width: 40vw; +} + +* { + font-family: Rubik, sans-serif; + box-sizing: border-box; +} + +.top-title { + text-align: center; +} + +@media only screen and (max-width: 600px) { + .page-container { + width: 100%; + } + + main { + width: 90%; + } +} diff --git a/src/lib/css/fonts.css b/src/lib/css/fonts.css new file mode 100644 index 0000000..206c47a --- /dev/null +++ b/src/lib/css/fonts.css @@ -0,0 +1,28 @@ +/* Generated using this application!! Dogfooding :) */ +@font-face { + font-family: "JetBrains Mono"; + src: url("https://cdn.sofiaritz.com/fonts/ofl/jetbrainsmono/JetBrainsMono[wght].ttf"); + font-style: normal; + font-weight: 400; +} + +@font-face { + font-family: "Rubik"; + src: url("https://cdn.sofiaritz.com/fonts/ofl/rubik/Rubik[wght].ttf"); + font-style: normal; + font-weight: 400 500; +} + +@font-face { + font-family: "Rubik"; + src: url("https://cdn.sofiaritz.com/fonts/ofl/rubik/Rubik[wght].ttf"); + font-style: italic; + font-weight: 400 500; +} + +@font-face { + font-family: "Rubik"; + src: url("https://cdn.sofiaritz.com/fonts/ofl/rubik/Rubik-Italic[wght].ttf"); + font-style: italic; + font-weight: 400; +} diff --git a/src/lib/css2.ts b/src/lib/css2.ts new file mode 100644 index 0000000..d4cb2db --- /dev/null +++ b/src/lib/css2.ts @@ -0,0 +1,52 @@ +export type Variant = { weight: number, style: string } + +export interface Family { + font: string, + variants: Variant[], +} + +/** + * @param input The input should look like this: `Fira+Sans:ital,wght@1,200;1,500` + */ +export function parse_family(input: string): Family { + let [rfont, rvariants] = input.split(":") + + let font = rfont.replaceAll("+", "").toLowerCase() + let variants: Variant[] = [] + if(rvariants != null && rvariants.length > 0) { + let rfragments = rvariants.split("wght@").filter(v => v.length > 0) + let fragments + if (rfragments[0].startsWith("ital")) { + fragments = rfragments[1] + } else { + fragments = rfragments[0] + } + + let rsubvariants = fragments.split(";") + for (let rvariant of rsubvariants) { + if (rvariant.includes(",")) { + let variant = rvariant.split(",") + variants.push({ + weight: Number(variant[1]), + style: Number(variant[0]) === 1 ? "italic" : "normal", + }) + } else { + variants.push({ + weight: Number(rvariant), + style: "normal", + }) + } + } + } else { + // TODO(sofia@git.sofiaritz.com): Is this the default value? + variants.push({ + weight: 400, + style: "normal", + }) + } + + return { + font, + variants, + } +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..b82c67f --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,130 @@ +import {parse} from "pb-parser"; +import {parse_family, type Variant} from "./css2"; + +interface MirrorVariant { + path: string, + name: string, + weight: number, + style: string, +} + +type MirrorFamily = MirrorVariant[] +type MirrorMetadata = MirrorFamily[] + +interface MirrorVariantUnifiedWeights { + path: string, + name: string, + weights: number[], + style: string +} + +type MirrorFamilyUnifiedWeights = MirrorVariantUnifiedWeights[] + +function to_array(v) { + if (Array.isArray(v)) { + return v + } else { + return [v] + } +} + +function font_base_url(font, mirror) { + return `${mirror}/ofl/${font}` +} +async function get_font_metadata(font, mirror) { + let response = await (await fetch(font_base_url(font, mirror) + "/METADATA.pb")) + .text() + + return parse(response) +} + +export async function css2_to_cssfile(css2_url, mirror): Promise { + let url = css2_url + .replace("https://fonts.googleapis.com/css2", "") + .replaceAll("&display=swap", "") + .replaceAll("&", "?") + + let rfamilies = url.split("?family=") + rfamilies.shift() + let mirror_meta = await Promise.all(rfamilies + .map(v => v.replace("?family=", "")) + .map(parse_family) + .map(async (v) => { + let metadata = await get_font_metadata(v.font, mirror) + let fonts = [] + for (let variant of v.variants) { + let { weight, style }: Variant = variant + let rfonts = to_array(metadata.fonts) + let matched = false + for (let font of rfonts) { + if (font.weight === weight) { + if (style === "italic") { + fonts.push({ + name: metadata.name, + path: font_base_url(v.font, mirror) + "/" + font.filename, + weight, + style, + }) + matched = true + } + } + } + + if (matched === false) { + fonts.push({ + name: metadata.name, + path: font_base_url(v.font, mirror) + "/" + rfonts[0].filename, + weight, + style, + }) + } + } + + return fonts + })) + + return mirror_metadata_to_css(mirror_meta) +} + +function mirror_metadata_to_css(mirror_meta: MirrorMetadata): string { + let css = "" + for (let family of mirror_meta) { + for (let variant of unify_weights(family)) { + css += +`@font-face { + font-family: "${variant.name}"; + src: url("${variant.path}"); + font-style: ${variant.style}; + font-weight: ${variant.weights.join(" ")}; +} + +` + } + } + + return css.trim() +} + +function unify_weights(family: MirrorFamily): MirrorFamilyUnifiedWeights { + let unified: MirrorFamilyUnifiedWeights = [] + for (let variant of family) { + let added = false + for (let uni_val of unified) { + if (uni_val.name === variant.name && uni_val.style === variant.style && uni_val.path === variant.path) { + uni_val.weights.push(variant.weight) + added = true + } + } + + if (added === false) { + unified.push({ + name: variant.name, + style: variant.style, + path: variant.path, + weights: [variant.weight], + }) + } + } + + return unified +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e98c083 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,9 @@ +import "./lib/css/fonts.css" +import "./lib/css/app.css" +import App from "./App.svelte" + +const app = new App({ + target: document.getElementById("app"), +}) + +export default app diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..dc6efba --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte" + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c4e1c5f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "isolatedModules": true + }, + "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..494bfe0 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler" + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..c8717db --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite" +import { svelte } from "@sveltejs/vite-plugin-svelte" + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [svelte()], +})