Initial commit

This commit is contained in:
Sofía Aritz 2023-05-15 20:17:18 +02:00
commit a7dedf5640
16 changed files with 542 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@ -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/

69
README.md Normal file
View File

@ -0,0 +1,69 @@
# GFonts Mirror Interface
<small>I need a better name :p</small>
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)!

20
index.html Normal file
View File

@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Google Fonts Mirror Interface</title>
</head>
<body>
<header class="top-title">
<h1>Google Fonts Mirror Interface</h1>
<span>
Transform <a href="https://fonts.google.com/">Google Fonts</a> links to a CSS from a <a href="https://cdn.sofiaritz.com/fonts/">Google Fonts binary mirror</a>.
Created by <a href="https://sofiaritz.com">Sofía Aritz</a>.
</span>
</header>
<div class="page-container">
<main id="app"></main>
<script type="module" src="/src/main.ts"></script>
</div>
</body>
</html>

25
package.json Normal file
View File

@ -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"
}
}

34
src/App.svelte Normal file
View File

@ -0,0 +1,34 @@
<script>
import {css2_to_cssfile} from "./lib/utils.ts";
import PreCopy from "./lib/PreCopy.svelte";
let gf_input = "https://fonts.googleapis.com/css2?family=Fira+Sans:ital,wght@0,300;1,200;1,500&family=Poppins:wght@300;400&family=Wix+Madefor+Display:wght@700&display=swap"
let mirror_input = "https://cdn.sofiaritz.com/fonts"
let output = new Promise(resolve => resolve(null))
function transform() {
let mirror = mirror_input
if (mirror.endsWith("/")) {
mirror = mirror.slice(0, -1)
}
output = css2_to_cssfile(gf_input, mirror)
}
</script>
<h3>Mirror URL</h3>
<input bind:value={mirror_input} type="url">
<h3>Google Fonts URL</h3>
<input bind:value={gf_input} type="url">
<button on:click={transform}>Transform</button>
{#await output}
{:then v}
{#if v != null}
<PreCopy>{v}</PreCopy>
{/if}
{:catch e}
<div>Error :( {e}</div>
{/await}

17
src/lib/PreCopy.svelte Normal file
View File

@ -0,0 +1,17 @@
<script>
let slot
function copy_code() {
navigator.clipboard.writeText(slot?.innerText)
}
</script>
<pre>
<button on:click={copy_code}>Copy code</button>
<span bind:this={slot}><slot></slot></span>
</pre>
<style>
button {
margin-left: 0;
}
</style>

83
src/lib/css/app.css Normal file
View File

@ -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%;
}
}

28
src/lib/css/fonts.css Normal file
View File

@ -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;
}

52
src/lib/css2.ts Normal file
View File

@ -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,
}
}

130
src/lib/utils.ts Normal file
View File

@ -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<string> {
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
}

9
src/main.ts Normal file
View File

@ -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

2
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

7
svelte.config.js Normal file
View File

@ -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(),
}

20
tsconfig.json Normal file
View File

@ -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" }]
}

9
tsconfig.node.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from "vite"
import { svelte } from "@sveltejs/vite-plugin-svelte"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
})