Skip to content

Commit ad28aec

Browse files
committed
feat: introduce font-lekton — optimized & Nerd Font Lekton with Bold Italic, dotted 0, squared-up bullet, plus build/CI/release
- synthesize the missing Bold Italic ( italic shapes + bold weight ) - dot the 0 and enlarge the • to a square across all four faces - ship desktop ( optimized/ ) and Nerd Font ( NerdFonts/ ) builds from the vendor originals - add FontForge scripts ( bolditalic, dotzero, preview ) and a one-click build.sh - wire up semantic-release + pre-commit workflows, OFL license, README banner, and CONTRIBUTING Signed-off-by: marslo <marslo.jiao@gmail.com>
0 parents  commit ad28aec

28 files changed

Lines changed: 1345 additions & 0 deletions

.github/workflows/pre-commit.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: pre-commit
2+
3+
# two scopes:
4+
# pr — PRs into main: only the changed range ( base..head )
5+
# full — pushes to main ( merged PR or direct ): every file
6+
on:
7+
pull_request:
8+
branches: [main]
9+
push:
10+
branches: [main]
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
pr:
17+
name: pre-commit ( PR diff range )
18+
if: ${{ github.event_name == 'pull_request' }}
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0 # need base.sha + head.sha history for the range
24+
25+
- uses: actions/setup-python@v5
26+
with:
27+
python-version: '3.x'
28+
29+
- name: Cache pre-commit envs
30+
uses: actions/cache@v4
31+
with:
32+
path: ~/.cache/pre-commit
33+
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
34+
35+
- name: Install pre-commit
36+
run: pip install pre-commit
37+
38+
- name: Run ( changed range only )
39+
run: |
40+
pre-commit run \
41+
--show-diff-on-failure --color=always \
42+
--from-ref "${{ github.event.pull_request.base.sha }}" \
43+
--to-ref "${{ github.event.pull_request.head.sha }}"
44+
45+
full:
46+
name: pre-commit ( all files )
47+
if: ${{ github.event_name == 'push' }}
48+
runs-on: ubuntu-latest
49+
steps:
50+
- uses: actions/checkout@v4
51+
52+
- uses: actions/setup-python@v5
53+
with:
54+
python-version: '3.x'
55+
56+
- name: Cache pre-commit envs
57+
uses: actions/cache@v4
58+
with:
59+
path: ~/.cache/pre-commit
60+
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
61+
62+
- name: Install pre-commit
63+
run: pip install pre-commit
64+
65+
- name: Run ( all files )
66+
run: pre-commit run --all-files --show-diff-on-failure --color=always

.github/workflows/release.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: release
2+
3+
# semantic-release on main: tag + CHANGELOG + GitHub release, with the two font
4+
# zips attached. releaserc lives in .releaserc.js.
5+
on:
6+
push:
7+
branches: [main]
8+
9+
permissions:
10+
contents: write # push CHANGELOG + create tags/releases
11+
issues: write # semantic-release comments
12+
pull-requests: write # semantic-release comments
13+
14+
concurrency:
15+
group: release-${{ github.ref }}
16+
cancel-in-progress: false
17+
18+
jobs:
19+
release:
20+
runs-on: ubuntu-latest
21+
# skip the release commit semantic-release pushes back, to avoid a loop
22+
if: ${{ !startsWith(github.event.head_commit.message, 'chore(release):') }}
23+
steps:
24+
- name: Checkout ( full history + tags )
25+
uses: actions/checkout@v4
26+
with:
27+
fetch-depth: 0
28+
persist-credentials: true
29+
30+
- name: Setup Node
31+
uses: actions/setup-node@v4
32+
with:
33+
node-version: 20
34+
35+
- name: Setup Python ( for the pre-commit prepareCmd )
36+
uses: actions/setup-python@v5
37+
with:
38+
python-version: '3.x'
39+
40+
- name: Install pre-commit
41+
run: pip install pre-commit
42+
43+
- name: Build font zips
44+
run: |
45+
zip -r Lekton-Optimized.zip optimized
46+
zip -r Lekton-NerdFont.zip NerdFonts
47+
ls -la Lekton-*.zip
48+
49+
- name: Install semantic-release
50+
run: |
51+
npm install --no-save \
52+
semantic-release \
53+
@semantic-release/commit-analyzer \
54+
@semantic-release/release-notes-generator \
55+
@semantic-release/changelog \
56+
@semantic-release/exec \
57+
@semantic-release/git \
58+
@semantic-release/github \
59+
conventional-changelog-conventionalcommits
60+
61+
- name: Run semantic-release
62+
env:
63+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
64+
run: npx semantic-release

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# python
2+
__pycache__/
3+
*.pyc
4+
5+
# node ( release workflow )
6+
node_modules/
7+
package-lock.json
8+
9+
# release artifacts ( built in CI, attached to the GitHub release )
10+
Lekton-Optimized.zip
11+
Lekton-NerdFont.zip

.pre-commit-config.yaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# yamllint disable rule:indentation
2+
---
3+
ci:
4+
autofix_prs: false
5+
6+
default_install_hook_types: [pre-commit, commit-msg]
7+
default_stages: [pre-commit]
8+
9+
repos:
10+
- repo: https://github.com/pre-commit/pre-commit-hooks
11+
rev: v6.0.0
12+
hooks:
13+
- id: trailing-whitespace
14+
name: Trim Trailing Whitespace
15+
args: [--markdown-linebreak-ext=md]
16+
- id: end-of-file-fixer
17+
name: End Of File Fixer
18+
- id: check-yaml
19+
name: Check YAML
20+
args: ["--unsafe"]
21+
- id: check-json
22+
name: Check JSON
23+
- id: check-merge-conflict
24+
name: Check Merge Conflict
25+
- id: check-case-conflict
26+
name: Check Case Conflict
27+
- id: mixed-line-ending
28+
name: Mixed Line Ending
29+
args: ["--fix=lf"]
30+
31+
- repo: https://github.com/astral-sh/ruff-pre-commit
32+
rev: v0.16.6
33+
hooks:
34+
- id: ruff-check
35+
name: Ruff (lint)
36+
args: ["--fix"]
37+
38+
- repo: https://github.com/crate-ci/typos
39+
rev: v1.50.1
40+
hooks:
41+
- id: typos
42+
name: Typos
43+
args: ['--write-changes', '--force-exclude']
44+
# scan the commit message itself; no --write-changes -> only fail, never rewrite the message
45+
- id: typos
46+
name: Typos (commit message)
47+
alias: typos-commit-msg
48+
stages: [commit-msg]
49+
args: ['--force-exclude']

.releaserc.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// =============================================================================
2+
// Self-contained changelog config.
3+
//
4+
// WHY THE TEMPLATES ARE INLINED HERE:
5+
// @semantic-release/release-notes-generator renders with the Handlebars-based conventional-changelog-writer.
6+
// conventional-changelog-conventionalcommits >= 9 dropped its Handlebars writerOpts in favour of @conventional-changelog/ template, which release-notes-generator does NOT use — so with preset >= 9 the section grouping silently disappears (flat list, no "### Features").
7+
// To stay compatible with ANY preset version (8, 9, 10, …) we supply the full Handlebars writerOpts (mainTemplate / headerPartial / commitPartial) and the grouping ourselves; the preset is then only used for its commit PARSER.
8+
// =============================================================================
9+
10+
const SECTIONS = [
11+
{ type: 'feat', section: 'Features' },
12+
{ type: 'fix', section: 'Bug Fixes' },
13+
{ type: 'refactor', section: 'Code Refactoring' },
14+
{ type: 'style', section: 'Styles' },
15+
{ type: 'chore', section: 'Others' },
16+
{ type: 'docs', section: 'Documentation' },
17+
{ type: 'perf', section: 'Performance' },
18+
{ type: 'test', section: 'Tests' },
19+
{ type: 'ci', section: 'CI/CD' }
20+
];
21+
const TYPE_TO_SECTION = Object.fromEntries(SECTIONS.map(s => [s.type, s.section]));
22+
const SECTION_ORDER = SECTIONS.map(s => s.section);
23+
const isSignoff = (line) => /^\s*Signed-off-by:/i.test(line);
24+
const stripSignoff = (text) => (text || '').split('\n').filter(l => !isSignoff(l)).join('\n').trim();
25+
26+
// ── Handlebars templates (writer-8 compatible; vendored from the conventional-commits preset so they work regardless of the installed preset major) ──
27+
const mainTemplate = `{{> header}}
28+
{{#if noteGroups}}
29+
{{#each noteGroups}}
30+
31+
### ⚠ {{title}}
32+
33+
{{#each notes}}
34+
* {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{text}}
35+
{{/each}}
36+
{{/each}}
37+
{{/if}}
38+
{{#each commitGroups}}
39+
40+
{{#if title}}
41+
### {{title}}
42+
43+
{{/if}}
44+
{{#each commits}}
45+
{{> commit root=@root}}
46+
{{/each}}
47+
{{/each}}
48+
`;
49+
50+
const headerPartial = `## {{#if @root.linkCompare~}}
51+
[{{version}}]({{~@root.host}}/{{#if this.owner}}{{~this.owner}}{{else}}{{~@root.owner}}{{/if}}/{{#if this.repository}}{{~this.repository}}{{else}}{{~@root.repository}}{{/if}}/compare/{{previousTag}}...{{currentTag}})
52+
{{~else}}
53+
{{~version}}
54+
{{~/if}}
55+
{{~#if date}} ({{date}})
56+
{{/if}}
57+
`;
58+
59+
// commit line: bold each comma-split scope as **scope:** (built as scopeFmt in transform) and keep the subject. PR commits carry an inline "(#N)" that GitHub auto-links, so leave them link-free; only direct pushes (no "(#N)") get an appended ([shortHash](…/commit/<hash>)) link. body/footer follow (built in transform)
60+
const commitPartial =
61+
'*{{#if scopeFmt}} {{scopeFmt}}:{{/if}} {{#if subject}}{{subject}}{{else}}{{header}}{{/if}}' +
62+
'{{#unless hasIssueRef}}{{#if @root.linkReferences}} ([{{shortHash}}]({{@root.host}}/{{@root.owner}}/{{@root.repository}}/commit/{{hash}})){{/if}}{{/unless}}' +
63+
'\n{{#if body}}\n{{body}}\n{{/if}}\n{{#if footer}}\n\n{{footer}}\n{{/if}}\n';
64+
65+
// ── dynamic changelog title ──
66+
// reuse an existing level-1 header (`# ...`) at the very top of CHANGELOG.md so new releases are inserted BELOW it; if there is none, leave changelogTitle unset so semantic-release just prepends (no title is forced onto title-less changelogs)
67+
const fs = require('fs');
68+
const path = require('path');
69+
const CHANGELOG_FILE = 'CHANGELOG.md';
70+
function detectChangelogTitle(file) {
71+
try {
72+
const text = fs.readFileSync(path.resolve(process.cwd(), file), 'utf8');
73+
const first = text.split('\n').find(l => l.trim() !== '');
74+
// a single '#' ATX header (rejects '##', '###', …) with actual text
75+
if (first && /^#\s+\S/.test(first)) { return first.trim(); }
76+
} catch (e) { /* missing or unreadable → treat as no title */ }
77+
return null;
78+
}
79+
const CHANGELOG_TITLE = detectChangelogTitle(CHANGELOG_FILE);
80+
81+
module.exports = {
82+
"branches": ["main"],
83+
"tagFormat": "v${version}",
84+
"plugins": [
85+
["@semantic-release/commit-analyzer", {
86+
"preset": "conventionalcommits",
87+
"releaseRules": [
88+
// { "breaking": true, "release": "minor" },
89+
{ "breaking": true, "release": "major" },
90+
// { "type": "feat", "release": "patch" },
91+
{ "type": "chore", "release": "patch" },
92+
{ "type": "refactor", "release": "patch" },
93+
{ "type": "style", "release": "patch" },
94+
{ "type": "docs", "release": "patch" },
95+
{ "type": "ci", "release": "patch" }
96+
]
97+
}],
98+
["@semantic-release/release-notes-generator", {
99+
"preset": "conventionalcommits",
100+
"presetConfig": { "types": SECTIONS },
101+
"writerOpts": {
102+
"groupBy": "type",
103+
// order sections as listed in SECTIONS (not alphabetically); unknown types go last
104+
"commitGroupsSort": (a, b) => {
105+
const rank = (t) => { const i = SECTION_ORDER.indexOf(t); return i === -1 ? SECTION_ORDER.length : i; };
106+
return rank(a.title) - rank(b.title);
107+
},
108+
"commitsSort": ["header", "subject"],
109+
"noteGroupsSort": "title",
110+
"mainTemplate": mainTemplate,
111+
"headerPartial": headerPartial,
112+
"commitPartial": commitPartial,
113+
"footerPartial": "",
114+
"transform": (commit) => {
115+
const c = { ...commit };
116+
117+
// type -> section label (drives the "### <section>" grouping)
118+
if (TYPE_TO_SECTION[c.type]) {
119+
c.type = TYPE_TO_SECTION[c.type];
120+
}
121+
122+
// preset's default transform (overridden here) normally sets shortHash; restore it so the commit link text isn't empty ("[](…/commit/<hash>)")
123+
if (typeof c.hash === 'string') {
124+
c.shortHash = c.hash.substring(0, 7);
125+
}
126+
127+
// PR commits carry an inline "(#N)" that GitHub auto-links; keep it verbatim and skip the sha link. direct pushes (no "(#N)") get the commit-sha link (see commitPartial)
128+
c.hasIssueRef = /\(#\d+\)/.test(c.subject || '');
129+
130+
// comma-split the scope and bold each part -> "**a**, **b**" (rendered via scopeFmt)
131+
if (c.scope) {
132+
c.scopeFmt = c.scope.split(',').map(s => '**' + s.trim() + '**').join(', ');
133+
}
134+
135+
// the parser may split trailing body lines into `footer` (e.g. a bullet containing an issue-like "#N"); fold body + footer back together and drop Signed-off-by
136+
const detail = [c.body, c.footer]
137+
.filter(Boolean)
138+
.join('\n')
139+
.split('\n')
140+
.filter(l => !isSignoff(l));
141+
if (detail.some(l => l.trim() !== '')) {
142+
// bullet body (`- ...`) -> 2-space sub-list right under the subject; free-form body (no bullets) -> blank line + 4-space verbatim block
143+
const hasBullets = detail.some(l => /^\s*-\s+\S/.test(l));
144+
const indent = hasBullets ? ' ' : ' ';
145+
const block = detail.map(l => l.trim() === '' ? '' : indent + l).join('\n');
146+
c.body = hasBullets ? block : '\n' + block;
147+
} else {
148+
c.body = null;
149+
}
150+
c.footer = null;
151+
152+
// drop Signed-off-by trailers from notes
153+
if (Array.isArray(c.notes)) {
154+
c.notes = c.notes
155+
.map(n => ({ ...n, text: stripSignoff(n.text) }))
156+
.filter(n => n.text);
157+
}
158+
159+
return c;
160+
}
161+
}
162+
}],
163+
["@semantic-release/changelog", Object.assign(
164+
{ "changelogFile": CHANGELOG_FILE },
165+
CHANGELOG_TITLE ? { "changelogTitle": CHANGELOG_TITLE } : {}
166+
)],
167+
["@semantic-release/exec", {
168+
"prepareCmd": "pre-commit run --files CHANGELOG.md || true"
169+
}],
170+
["@semantic-release/git", {
171+
"assets": ["CHANGELOG.md"],
172+
"message": "chore(release): v${nextRelease.version}"
173+
}],
174+
["@semantic-release/github", {
175+
// the two font zips are built by the release workflow before semantic-release runs
176+
"assets": [
177+
{ "path": "Lekton-Optimized.zip", "label": "Lekton-Optimized — desktop faces (dotted 0, big bullet, + Bold Italic)" },
178+
{ "path": "Lekton-NerdFont.zip", "label": "Lekton-NerdFont — Nerd Font Mono faces (otf + ttf)" }
179+
]
180+
}]
181+
]
182+
};

0 commit comments

Comments
 (0)