Initial commit
This commit is contained in:
commit
a7dedf5640
16 changed files with 542 additions and 0 deletions
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal 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
69
README.md
Normal 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
20
index.html
Normal 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
25
package.json
Normal 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
34
src/App.svelte
Normal 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
17
src/lib/PreCopy.svelte
Normal 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
83
src/lib/css/app.css
Normal 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
28
src/lib/css/fonts.css
Normal 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
52
src/lib/css2.ts
Normal 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
130
src/lib/utils.ts
Normal 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
9
src/main.ts
Normal 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
2
src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
7
svelte.config.js
Normal file
7
svelte.config.js
Normal 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
20
tsconfig.json
Normal 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
9
tsconfig.node.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
7
vite.config.ts
Normal file
7
vite.config.ts
Normal 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()],
|
||||
})
|
Loading…
Reference in a new issue