diff --git a/package.json b/package.json index 8254e55..e9b320d 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,10 @@ "devDependencies": { "@bschlenk/eslint-config": "^0.0.3", "@otfjs/cli": "workspace:*", - "eslint": "^9.14.0", - "eslint-plugin-react-refresh": "^0.4.14", - "prettier": "^3.3.3", - "prettier-plugin-tailwindcss": "^0.6.8", - "typescript": "^5.6.3" + "eslint": "^9.25.1", + "eslint-plugin-react-refresh": "^0.4.20", + "prettier": "^3.5.3", + "prettier-plugin-tailwindcss": "^0.6.11", + "typescript": "^5.8.3" } } diff --git a/packages/otfjs-cli/package.json b/packages/otfjs-cli/package.json index c12e362..78f6d07 100644 --- a/packages/otfjs-cli/package.json +++ b/packages/otfjs-cli/package.json @@ -24,13 +24,13 @@ "dependencies": { "@bschlenk/mat": "^0.0.11", "@bschlenk/vec": "^0.0.7", - "himalaya": "^1.1.0", + "himalaya": "^1.1.1", "otfjs": "workspace:*", "svgo": "4.0.0-rc.1" }, "devDependencies": { - "@types/node": "^22.8.7", - "typescript": "^5.6.3", - "vitest": "^2.1.4" + "@types/node": "^22.15.0", + "typescript": "^5.8.3", + "vitest": "^3.1.2" } } diff --git a/packages/otfjs-cli/src/@types/himalaya.d.ts b/packages/otfjs-cli/src/@types/himalaya.d.ts index c70bb67..18d2c3b 100644 --- a/packages/otfjs-cli/src/@types/himalaya.d.ts +++ b/packages/otfjs-cli/src/@types/himalaya.d.ts @@ -24,5 +24,8 @@ declare module 'himalaya' { export type Node = Element | Comment | Text export function parse(str: string, options?: any): Node[] - export function stringify(elements: Node[]): string + export function stringify( + elements: Node[], + options?: { preferDoubleQuoteAttributes?: boolean }, + ): string } diff --git a/packages/otfjs-cli/src/cmd/gen-previews.ts b/packages/otfjs-cli/src/cmd/gen-previews.ts index aa398fe..60455a4 100644 --- a/packages/otfjs-cli/src/cmd/gen-previews.ts +++ b/packages/otfjs-cli/src/cmd/gen-previews.ts @@ -24,32 +24,36 @@ export async function run(args: string[]) { async function processFonts(fontFiles: string[], outDir: string) { for await (const { font, file } of iterFonts(fontFiles)) { - console.log(file) - - const preview = generatePreview(font) - if (!preview) { - console.warn(`failed to generate preview for ${file}`) - continue - } - - const name = path.basename(file, path.extname(file)) - const outPath = path.join(outDir, `${name}.svg`) - - const optimized = optimize(preview, { - path: outPath, - multipass: true, - plugins: [ - 'preset-default', - { - name: 'prefixIds', - params: { - prefix: name.toLowerCase().replaceAll(' ', '-'), - delim: '-', + try { + console.log(file) + + const preview = generatePreview(font) + if (!preview) { + console.warn(`failed to generate preview for ${file}`) + continue + } + + const name = path.basename(file, path.extname(file)) + const outPath = path.join(outDir, `${name}.svg`) + + const optimized = optimize(preview, { + path: outPath, + multipass: true, + plugins: [ + 'preset-default', + { + name: 'prefixIds', + params: { + prefix: name.toLowerCase().replaceAll(' ', '-'), + delim: '-', + }, }, - }, - ], - }) - await fs.writeFile(outPath, optimized.data) + ], + }) + await fs.writeFile(outPath, optimized.data) + } catch (err) { + console.error(`failed to generate preview for ${file}:`, err) + } } } diff --git a/packages/otfjs-cli/src/cmd/sprite.ts b/packages/otfjs-cli/src/cmd/sprite.ts index c27d09a..768bdbc 100644 --- a/packages/otfjs-cli/src/cmd/sprite.ts +++ b/packages/otfjs-cli/src/cmd/sprite.ts @@ -6,15 +6,13 @@ import { Element, parse, stringify } from 'himalaya' import { stripExt } from '../lib/cli.js' export function run(args: string[]) { - const dir = args[0] + const [dir, outDir, complexPath] = args + const previewFile = path.join(outDir, 'preview.svg') const files = fs.readdirSync(dir) + const complexIds: string[] = [] - const root: Element = { - type: 'element', - tagName: 'svg', - attributes: [{ key: 'xmlns', value: 'http://www.w3.org/2000/svg' }], - children: [], - } + const stream = fs.createWriteStream(previewFile) + stream.write('\n') for (const file of files) { const data = fs.readFileSync(path.join(dir, file), 'utf-8') @@ -25,17 +23,32 @@ export function run(args: string[]) { continue } + const fileId = fileToId(file) + + // We merge all defs into a single defs element at the start of the svg sprite + const defsIndex = svg.children.findIndex( + (child) => child.type === 'element' && child.tagName === 'defs', + ) + + if (defsIndex !== -1) { + fs.writeFileSync(path.join(outDir, `${fileId}.svg`), data) + complexIds.push(fileId) + continue + } + const viewBox = svg.attributes.find((attr) => attr.key === 'viewBox')! + const id = { key: 'id', value: fileId } - root.children.push({ - type: 'element', - tagName: 'symbol', - attributes: [{ key: 'id', value: fileToId(file) }, viewBox], - children: svg.children, - }) + svg.attributes = [id, viewBox] + svg.tagName = 'symbol' + + stream.write(stringify([svg])) + stream.write('\n') } - console.log(stringify([root])) + stream.end('\n') + + fs.writeFileSync(complexPath, JSON.stringify(complexIds)) } function fileToId(file: string) { diff --git a/packages/otfjs-ui/package.json b/packages/otfjs-ui/package.json index d549c88..a19e472 100644 --- a/packages/otfjs-ui/package.json +++ b/packages/otfjs-ui/package.json @@ -16,18 +16,18 @@ "@bschlenk/vec": "^0.0.7", "clsx": "^2.1.1", "otfjs": "workspace:^", - "react": "^18.3.1", - "react-dom": "^18.3.1" + "react": "^19.1.0", + "react-dom": "^19.1.0" }, "devDependencies": { - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.3.3", - "autoprefixer": "^10.4.20", - "postcss": "^8.4.47", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.3", "svg-sprite-generator": "^0.0.7", "tailwindcss": "^3.4.14", - "typescript": "^5.6.3", - "vite": "^5.4.10" + "typescript": "^5.8.3", + "vite": "^6.3.3" } } diff --git a/packages/otfjs-ui/public/blaka-ink.svg b/packages/otfjs-ui/public/blaka-ink.svg new file mode 100644 index 0000000..1bffcbe --- /dev/null +++ b/packages/otfjs-ui/public/blaka-ink.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/otfjs-ui/public/bungee-spice.svg b/packages/otfjs-ui/public/bungee-spice.svg new file mode 100644 index 0000000..b53463a --- /dev/null +++ b/packages/otfjs-ui/public/bungee-spice.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/otfjs-ui/public/foldit.svg b/packages/otfjs-ui/public/foldit.svg new file mode 100644 index 0000000..77341e8 --- /dev/null +++ b/packages/otfjs-ui/public/foldit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/otfjs-ui/public/honk.svg b/packages/otfjs-ui/public/honk.svg new file mode 100644 index 0000000..89b67c4 --- /dev/null +++ b/packages/otfjs-ui/public/honk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/otfjs-ui/public/kalnia-glaze.svg b/packages/otfjs-ui/public/kalnia-glaze.svg new file mode 100644 index 0000000..a77c094 --- /dev/null +++ b/packages/otfjs-ui/public/kalnia-glaze.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/otfjs-ui/public/nabla.svg b/packages/otfjs-ui/public/nabla.svg new file mode 100644 index 0000000..ce33e89 --- /dev/null +++ b/packages/otfjs-ui/public/nabla.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/otfjs-ui/public/preview.svg b/packages/otfjs-ui/public/preview.svg index 4f3a144..f9f5539 100644 --- a/packages/otfjs-ui/public/preview.svg +++ b/packages/otfjs-ui/public/preview.svg @@ -1 +1,1826 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/otfjs-ui/src/components/font-context.tsx b/packages/otfjs-ui/src/components/font-context.tsx index 4dc9745..f1203ee 100644 --- a/packages/otfjs-ui/src/components/font-context.tsx +++ b/packages/otfjs-ui/src/components/font-context.tsx @@ -5,7 +5,7 @@ import { useMemo, useState, } from 'react' -import { Font, isWoff2 } from 'otfjs' +import { Font, isWoff2, NameId } from 'otfjs' import { HasChildren } from '../types/has-children' import { noop } from '../utils/noop' @@ -54,9 +54,19 @@ export function useClearFont() { export function useLoadFont() { const setFont = useSetFont() - return useCallback((buff: ArrayBuffer) => { + return useCallback(async (buff: ArrayBuffer) => { // TODO: some kind of toast on failure? - void readFont(new Uint8Array(buff)).then(setFont) + try { + const font = await readFont(new Uint8Array(buff)) + setFont(font) + + const fontName = font.getName(NameId.FontFamilyName)! + const newFont = new FontFace(fontName, buff) + await newFont.load() + document.fonts.add(newFont) + } catch (e) { + console.error('Failed to load font', e) + } }, []) } diff --git a/packages/otfjs-ui/src/components/font-icon/font-icon.module.css b/packages/otfjs-ui/src/components/font-icon/font-icon.module.css new file mode 100644 index 0000000..7f65e29 --- /dev/null +++ b/packages/otfjs-ui/src/components/font-icon/font-icon.module.css @@ -0,0 +1,4 @@ +.imgPreview { + aspect-ratio: 1 / 1; + object-fit: contain; +} diff --git a/packages/otfjs-ui/src/components/font-icon/font-icon.tsx b/packages/otfjs-ui/src/components/font-icon/font-icon.tsx index 07b21eb..8b1c958 100644 --- a/packages/otfjs-ui/src/components/font-icon/font-icon.tsx +++ b/packages/otfjs-ui/src/components/font-icon/font-icon.tsx @@ -1,10 +1,28 @@ +import complexFontsArray from '../../fonts-complex.json' + +import styles from './font-icon.module.css' + export interface FontIconProps { name: string size: number } +const complexFonts = new Set(complexFontsArray) + export function FontIcon({ name, size }: FontIconProps) { const id = name.toLowerCase().replaceAll(' ', '-') + + if (complexFonts.has(id)) { + return ( + + ) + } + return ( diff --git a/packages/otfjs-ui/src/components/font-view/components/head.tsx b/packages/otfjs-ui/src/components/font-view/components/head.tsx new file mode 100644 index 0000000..6e0e335 --- /dev/null +++ b/packages/otfjs-ui/src/components/font-view/components/head.tsx @@ -0,0 +1,89 @@ +import { NameId } from 'otfjs' +import clsx from 'clsx' + +import { useClearFont, useFont } from '../../font-context' +import { FontViewMode, useFontView } from '../font-view-context' + +import styles from '../font-view.module.css' +import { IconButton } from '../../icon-button/icon-button' +import { Text } from '../../text' +import { IconBack } from '../../icons/icon-back' +import { FontIcon } from '../../font-icon/font-icon' +import { HasFont } from '../../../types/has-font' +import { IconLink } from '../../icons/icon-link' +import { sizeToSTring as sizeToString } from '../../../utils/bytes' + +export function Head({ tag }: { tag: string }) { + const font = useFont() + const clearFont = useClearFont() + const name = font.getName(NameId.FontFamilyName)! + const { setMode } = useFontView() + + return ( +
+
+
+ + + + +
+
+ +
+ + +
+
+
+
+ + +
+
+
+ + {tag} + +
+
+
+ ) +} + +function FontName({ font }: HasFont) { + const name = font.getName(NameId.FontFamilyName) + return

{name}

+} + +function GlyphCount({ font }: HasFont) { + return {font.numGlyphs} Glyphs +} + +function FileSize({ font }: HasFont) { + return ( + + {sizeToString(font.size)} + + ) +} + +function DocLink({ + tag, + children, +}: { + tag: string + children: React.ReactNode +}) { + const url = `https://learn.microsoft.com/en-us/typography/opentype/spec/${tag}` + return ( + + {children} + + ) +} diff --git a/packages/otfjs-ui/src/components/font-view/font-view-context.tsx b/packages/otfjs-ui/src/components/font-view/font-view-context.tsx new file mode 100644 index 0000000..f98a48d --- /dev/null +++ b/packages/otfjs-ui/src/components/font-view/font-view-context.tsx @@ -0,0 +1,39 @@ +import { createContext, useContext, useMemo, useState } from 'react' + +export const enum FontViewMode { + Inspect, + Type, +} + +interface FontViewContextValue { + mode: FontViewMode + setMode: React.Dispatch> +} + +const FontViewContext = createContext({ + mode: FontViewMode.Inspect, + setMode: () => {}, +}) + +export function FontViewProvider({ + value, + children, +}: { + value: FontViewContextValue + children: React.ReactNode +}) { + return ( + + {children} + + ) +} + +export function useFontViewState() { + const [mode, setMode] = useState(FontViewMode.Inspect) + return useMemo(() => ({ mode, setMode }), [mode]) +} + +export function useFontView() { + return useContext(FontViewContext) +} diff --git a/packages/otfjs-ui/src/components/font-view/font-view.module.css b/packages/otfjs-ui/src/components/font-view/font-view.module.css index 04b2af6..746dec6 100644 --- a/packages/otfjs-ui/src/components/font-view/font-view.module.css +++ b/packages/otfjs-ui/src/components/font-view/font-view.module.css @@ -21,6 +21,12 @@ align-items: center; } +.headSection { + display: flex; + align-items: center; + flex: 1 0 0; +} + .sidebar { grid-area: sidebar; border-right: 1px solid var(--color-border); diff --git a/packages/otfjs-ui/src/components/font-view/font-view.tsx b/packages/otfjs-ui/src/components/font-view/font-view.tsx index 8eccd59..0cc56fc 100644 --- a/packages/otfjs-ui/src/components/font-view/font-view.tsx +++ b/packages/otfjs-ui/src/components/font-view/font-view.tsx @@ -2,23 +2,23 @@ import { useState } from 'react' import clsx from 'clsx' import { Font, NameId } from 'otfjs' -import { HasFont } from '../../types/has-font' -import { sizeToSTring } from '../../utils/bytes' -import { useClearFont, useFont } from '../font-context' -import { FontIcon } from '../font-icon/font-icon' -import { IconButton } from '../icon-button/icon-button' -import { IconBack } from '../icons/icon-back' -import { IconLink } from '../icons/icon-link' -import { Text } from '../text' +import { useFont } from '../font-context' import { TABLE_MAP } from './font-view.utils' import styles from './font-view.module.css' +import { + FontViewMode, + FontViewProvider, + useFontViewState, +} from './font-view-context' +import { Head } from './components/head' interface FontViewProps { font: Font } export function FontView({ font }: FontViewProps) { + const state = useFontViewState() const [tag, setTag] = useState(() => font.hasTable('glyf') ? 'glyf' : font.hasTable('head') ? 'head' @@ -26,40 +26,50 @@ export function FontView({ font }: FontViewProps) { ) return ( -
- + +
+ + {state.mode === FontViewMode.Inspect ? + + : state.mode === FontViewMode.Type ? + + : null} +
+
+ ) +} + +function InspectMode({ + tag, + setTag, +}: { + tag: string + setTag: React.Dispatch> +}) { + return ( + <> -
+ ) } -function Head({ tag }: { tag: string }) { +function TypeMode() { const font = useFont() - const clearFont = useClearFont() - const name = font.getName(NameId.FontFamilyName)! + const fontFamily = font.getName(NameId.FontFamilyName) return ( -
-
- - - - + <> + {}} /> +
+