├── .gitattributes ├── .github ├── release_template.md └── workflows │ ├── custom.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── AUTHORS.txt ├── Dockerfile ├── OFL.txt ├── README.md ├── README_CN.md ├── README_JA.md ├── build.py ├── config.json ├── pyproject.toml ├── requirements.txt ├── resources ├── 2-1.png ├── 2-1.svg ├── header.png ├── header.svg └── showcase.png ├── source ├── MapleMono-Italic[wght]-VF.ttf ├── MapleMono-Italic[wght].glyphs ├── MapleMono-Italic[wght].vfc ├── MapleMono-NF-Base-Mono.ttf ├── MapleMono-NF-Base.ttf ├── MapleMono[wght]-VF.ttf ├── MapleMono[wght].glyphs ├── MapleMono[wght].vfc ├── cn │ └── static.sha256 ├── config.yaml ├── features │ ├── README.md │ ├── README_CN.md │ ├── cn.fea │ ├── italic.fea │ └── regular.fea ├── py │ ├── feature │ │ ├── README.md │ │ ├── __init__.py │ │ ├── ast.py │ │ ├── base │ │ │ ├── __init__.py │ │ │ ├── case.py │ │ │ ├── ccmp.py │ │ │ ├── clazz.py │ │ │ ├── lang.py │ │ │ ├── locl.py │ │ │ └── number.py │ │ ├── calt │ │ │ ├── __init__.py │ │ │ ├── _infinite_utils.py │ │ │ ├── asciitilde.py │ │ │ ├── cross.py │ │ │ ├── equal_arrow.py │ │ │ ├── escape.py │ │ │ ├── hyphen_arrow.py │ │ │ ├── italic.py │ │ │ ├── markup_like.py │ │ │ ├── pipe.py │ │ │ ├── tag.py │ │ │ └── whitespace │ │ │ │ ├── __init__.py │ │ │ │ ├── brace.py │ │ │ │ ├── colon.py │ │ │ │ ├── multiple_compare.py │ │ │ │ ├── numbersign_underscore.py │ │ │ │ └── upper.py │ │ ├── cv │ │ │ ├── _common.py │ │ │ ├── cv01.py │ │ │ ├── cv02.py │ │ │ ├── cv03.py │ │ │ ├── cv04.py │ │ │ ├── cv05.py │ │ │ ├── cv06.py │ │ │ ├── cv07.py │ │ │ ├── cv08.py │ │ │ ├── cv31.py │ │ │ ├── cv32.py │ │ │ ├── cv33.py │ │ │ ├── cv34.py │ │ │ ├── cv35.py │ │ │ ├── cv36.py │ │ │ ├── cv37.py │ │ │ ├── cv38.py │ │ │ ├── cv39.py │ │ │ ├── cv40.py │ │ │ ├── cv41.py │ │ │ ├── cv61.py │ │ │ ├── cv62.py │ │ │ ├── cv63.py │ │ │ ├── cv64.py │ │ │ ├── cv65.py │ │ │ ├── cv96.py │ │ │ ├── cv97.py │ │ │ ├── cv98.py │ │ │ └── cv99.py │ │ ├── italic.py │ │ ├── regular.py │ │ └── ss │ │ │ ├── ss01.py │ │ │ ├── ss02.py │ │ │ ├── ss03.py │ │ │ ├── ss04.py │ │ │ ├── ss05.py │ │ │ ├── ss06.py │ │ │ ├── ss07.py │ │ │ ├── ss08.py │ │ │ ├── ss09.py │ │ │ ├── ss10.py │ │ │ └── ss11.py │ ├── freeze.py │ ├── in_browser.py │ ├── task │ │ ├── _utils.py │ │ ├── fea.py │ │ ├── nerdfont.py │ │ ├── page.py │ │ └── release.py │ └── utils.py └── schema.json ├── task.py └── woff2 └── var ├── MapleMono-Italic[wght]-VF.woff2 └── MapleMono[wght]-VF.woff2 /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ttf binary 2 | *.vfc binary 3 | *.woff2 binary -------------------------------------------------------------------------------- /.github/release_template.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | 4 | 5 | ## Download 6 | 7 | [Which File to Download?](https://github.com/subframe7536/maple-font/tree/variable?tab=readme-ov-file#naming-faq) | [我该下载哪个文件?](https://github.com/subframe7536/maple-font/blob/variable/README_CN.md#%E5%91%BD%E5%90%8D%E8%AF%B4%E6%98%8E) 8 | 9 | If you want to get the font that similar to `JetBrains Mono`, please download the "Normal-Ligature" or "Normal-No-Ligature" 10 | 11 | 如果你想用的是和 `JetBrains Mono` 相近的字体,请下载 "Normal-Ligature" 或者 "Normal-No-Ligature" 12 | 13 | If you don't want to choose: [Click to download](https:///MapleMono-NF-unhinted.zip) 14 | 15 | 懒人包:[点击下载](https:///MapleMono-NF-CN-unhinted.zip) 16 | 17 | | Format | Ligature (default) | No-Ligature | Normal-Ligature | Normal-No-Ligature | 18 | | -------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | 19 | | Variable | [📦 Download](https:///MapleMono-Variable.zip) | [📦 Download](https:///MapleMonoNL-Variable.zip) | [📦 Download](https:///MapleMonoNormal-Variable.zip) | [📦 Download](https:///MapleMonoNormalNL-Variable.zip) | 20 | | TTF | [📦 Download](https:///MapleMono-TTF.zip) ([hinted](https:///MapleMono-TTF-AutoHint.zip)) | [📦 Download](https:///MapleMonoNL-TTF.zip) ([hinted](https:///MapleMonoNL-TTF-AutoHint.zip)) | [📦 Download](https:///MapleMonoNormal-TTF.zip) ([hinted](https:///MapleMonoNormal-TTF-AutoHint.zip)) | [📦 Download](https:///MapleMonoNormalNL-TTF.zip) ([hinted](https:///MapleMonoNormalNL-TTF-AutoHint.zip)) | 21 | | OTF | [📦 Download](https:///MapleMono-OTF.zip) | [📦 Download](https:///MapleMonoNL-OTF.zip) | [📦 Download](https:///MapleMonoNormal-OTF.zip) | [📦 Download](https:///MapleMonoNormalNL-OTF.zip) | 22 | | WOFF2 | [📦 Download](https:///MapleMono-Woff2.zip) | [📦 Download](https:///MapleMonoNL-Woff2.zip) | [📦 Download](https:///MapleMonoNormal-Woff2.zip) | [📦 Download](https:///MapleMonoNormalNL-Woff2.zip) | 23 | | NF | [📦 Download](https:///MapleMono-NF-unhinted.zip) ([hinted](https:///MapleMono-NF.zip)) | [📦 Download](https:///MapleMonoNL-NF-unhinted.zip) ([hinted](https:///MapleMonoNL-NF.zip)) | [📦 Download](https:///MapleMonoNormal-NF-unhinted.zip) ([hinted](https:///MapleMonoNormal-NF.zip)) | [📦 Download](https:///MapleMonoNormalNL-NF-unhinted.zip) ([hinted](https:///MapleMonoNormalNL-NF.zip)) | 24 | | CN | [📦 Download](https:///MapleMono-CN-unhinted.zip) ([hinted](https:///MapleMono-CN.zip)) | [📦 Download](https:///MapleMonoNL-CN-unhinted.zip) ([hinted](https:///MapleMonoNL-CN.zip)) | [📦 Download](https:///MapleMonoNormal-CN-unhinted.zip) ([hinted](https:///MapleMonoNormal-CN.zip)) | [📦 Download](https:///MapleMonoNormalNL-CN-unhinted.zip) ([hinted](https:///MapleMonoNormalNL-CN.zip)) | 25 | | NF-CN | [📦 Download](https:///MapleMono-NF-CN-unhinted.zip) ([hinted](https:///MapleMono-NF-CN.zip)) | [📦 Download](https:///MapleMonoNL-NF-CN-unhinted.zip) ([hinted](https:///MapleMonoNL-NF-CN.zip)) | [📦 Download](https:///MapleMonoNormal-NF-CN-unhinted.zip) ([hinted](https:///MapleMonoNormal-NF-CN.zip)) | [📦 Download](https:///MapleMonoNormalNL-NF-CN-unhinted.zip) ([hinted](https:///MapleMonoNormalNL-NF-CN.zip)) | -------------------------------------------------------------------------------- /.github/workflows/custom.yml: -------------------------------------------------------------------------------- 1 | name: Custom Build 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | cn: 7 | description: 'Include Chinese version (add "--cn" to build args)' 8 | required: false 9 | default: false 10 | type: boolean 11 | normal: 12 | description: 'Remove opinionated features (add "--normal" to build args)' 13 | required: false 14 | default: false 15 | type: boolean 16 | no_liga: 17 | description: 'Remove ligatures (add "--no-liga" to build args)' 18 | required: false 19 | default: false 20 | type: boolean 21 | no_hinted: 22 | description: 'Build unhinted font (add "--no-hinted" to build args)' 23 | required: false 24 | default: false 25 | type: boolean 26 | nf_mono: 27 | description: 'Fixed Nerd Font icon width (add "--nf-mono" to build args)' 28 | required: false 29 | default: false 30 | type: boolean 31 | feat: 32 | description: 'Enable features, split by `,`, e.g. "cv01,ss02" (add "--feat feat1,feat2" to build args)' 33 | required: false 34 | default: '' 35 | type: string 36 | build_args: 37 | description: 'Other args for build.py' 38 | required: false 39 | default: '' 40 | type: string 41 | 42 | permissions: 43 | contents: write 44 | 45 | jobs: 46 | custom-build: 47 | runs-on: ubuntu-latest 48 | 49 | steps: 50 | - name: Checkout repository 51 | uses: actions/checkout@v4 52 | 53 | - name: Setup Python 54 | uses: actions/setup-python@v5 55 | with: 56 | python-version: '3.12' 57 | cache: 'pip' 58 | cache-dependency-path: './requirements.txt' 59 | 60 | - name: Install dependencies 61 | run: | 62 | pip install -r requirements.txt 63 | 64 | # https://github.com/ryanoasis/nerd-fonts/blob/b3c1b0cf424ac32915cf4fd822c3938d4a815566/.github/workflows/release.yml#L144-L166 65 | if [[ "${{ github.event.inputs.build_args }}" == *"--font-patcher"* ]]; then 66 | sudo apt update -y -q 67 | sudo apt install software-properties-common -y -q 68 | sudo apt install python3-fontforge -y -q 69 | sudo apt install fuse -y -q 70 | 71 | curl -L "https://github.com/fontforge/fontforge/releases/download/20230101/FontForge-2023-01-01-a1dad3e-x86_64.AppImage" --output fontforge 72 | chmod u+x fontforge 73 | echo Try appimage 74 | ./fontforge --version 75 | export PATH=`pwd`:$PATH 76 | echo "PATH=$PATH" >> $GITHUB_ENV 77 | echo Try appimage with path 78 | fontforge --version 79 | fi 80 | 81 | - name: Run custom script 82 | run: | 83 | build_args="--archive" 84 | if [ "${{ github.event.inputs.cn }}" == "true" ]; then 85 | build_args="$build_args --cn" 86 | fi 87 | if [ "${{ github.event.inputs.normal }}" == "true" ]; then 88 | build_args="$build_args --normal" 89 | fi 90 | if [ "${{ github.event.inputs.no_liga }}" == "true" ]; then 91 | build_args="$build_args --no-liga" 92 | fi 93 | if [ "${{ github.event.inputs.no_hinted }}" == "true" ]; then 94 | build_args="$build_args --no-hinted" 95 | fi 96 | if [ "${{ github.event.inputs.nf_mono }}" == "true" ]; then 97 | build_args="$build_args --nf-mono" 98 | fi 99 | if [ -n "${{ github.event.inputs.feat }}" ]; then 100 | build_args="$build_args --feat ${{ github.event.inputs.feat }}" 101 | fi 102 | if [ -n "${{ github.event.inputs.build_args }}" ]; then 103 | build_args="$build_args ${{ github.event.inputs.build_args }}" 104 | fi 105 | 106 | echo "BUILD_ARGS=$build_args" >> $GITHUB_ENV 107 | python build.py $build_args 108 | continue-on-error: true 109 | - id: check_issue 110 | name: Check issue feature file 111 | run: | 112 | if [ -f fonts/issue.fea ] 113 | then 114 | echo 'exists=true' >> $GITHUB_OUTPUT 115 | else 116 | echo 'exists=false' >> $GITHUB_OUTPUT 117 | fi 118 | - name: Upload issue artifact 119 | if: ${{ steps.check_issue.outputs.exists == 'true' }} 120 | uses: actions/upload-artifact@v4 121 | with: 122 | name: issue-fea-file 123 | path: fonts/issue.fea 124 | - name: Create release 125 | run: | 126 | if [ ! -d "fonts/archive" ]; then 127 | echo "Error: Failed to build font, please check [Run custom script] step." 128 | exit 1 129 | fi 130 | echo "### Build arguments" >> NOTES 131 | echo "\`\`\`" >> NOTES 132 | echo "python build.py $BUILD_ARGS" >> NOTES 133 | echo "\`\`\`" >> NOTES 134 | echo "### Final Configuration" >> NOTES 135 | echo "\`\`\`" >> NOTES 136 | python build.py $BUILD_ARGS --dry >> NOTES 137 | echo "\`\`\`" >> NOTES 138 | gh release create "v$(date +%s)" fonts/archive/*.* --notes-file NOTES 139 | env: 140 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 141 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build All Formats and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | workflow_dispatch: 8 | inputs: 9 | build_args: 10 | description: 'Args for build.py' 11 | required: false 12 | default: '' 13 | type: string 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | build: 20 | strategy: 21 | matrix: 22 | config: [ 23 | {name: normal-liga, args: "--normal --liga"}, 24 | {name: normal-no-liga, args: "--normal --no-liga"}, 25 | {name: liga, args: "--liga"}, 26 | {name: no-liga, args: "--no-liga"} 27 | ] 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: actions/setup-python@v5 32 | with: 33 | python-version: '3.12' 34 | cache: 'pip' 35 | cache-dependency-path: './requirements.txt' 36 | - run: pip install -r requirements.txt 37 | - name: Build fonts 38 | run: | 39 | python build.py --archive --cn-both --hinted ${{ matrix.config.args }} ${{ github.event.inputs.build_args }} 40 | python build.py --archive --cn-both --no-hinted --cache ${{ matrix.config.args }} ${{ github.event.inputs.build_args }} 41 | - uses: actions/upload-artifact@v4 42 | with: 43 | name: release-artifacts-${{ matrix.config.name }} 44 | path: fonts/archive/* 45 | 46 | create-release: 47 | needs: build 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v4 51 | with: 52 | fetch-depth: 0 53 | - uses: actions/download-artifact@v4 54 | with: 55 | pattern: release-artifacts* 56 | path: release 57 | 58 | - name: Release artifacts 59 | run: | 60 | TAG="${{ github.ref_name }}" 61 | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then 62 | TAG="v$(date +%s)" 63 | PREV=$(git tag --list --sort=committerdate | tail -n 1) 64 | CHANGELOG=$(git log --pretty=format:"- %s" "$PREV"..HEAD | awk '1; END {print ""}' | tac) 65 | else 66 | PREV=$(git tag --list --sort=committerdate | tail -n 2 | head -n 1) 67 | CHANGELOG=$(git log --pretty=format:"- %s" "$PREV".."$TAG" | sed '1d' | awk '1; END {print ""}' | tac) 68 | fi 69 | sed -i 's||'"${CHANGELOG//$'\n'/\\n}"'|' .github/release_template.md 70 | 71 | sed -i "s|https://|https://github.com/subframe7536/maple-font/releases/download/${TAG//\//\\/}|g" .github/release_template.md 72 | 73 | 74 | TITLE=$(python3 -c "print(' '.join(part.capitalize() for part in '$TAG'.split('-')))") 75 | 76 | gh release create "$TAG" release/**/*.* --notes-file .github/release_template.md --draft -t "$TITLE" 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | FontPatcher 2 | fonts 3 | release 4 | source/cn/**/*.ttf 5 | cdn 6 | 7 | __pycache__ 8 | .DS_store 9 | *.zip 10 | .venv 11 | .vscode 12 | uv.lock 13 | *.backup -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "maple-font-page"] 2 | path = maple-font-page 3 | url = git@github.com:subframe7536/maple-font-page.git 4 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | subframe7536 <1667077010@qq.com> -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | # Install system dependencies including FontForge 4 | RUN apt-get update \ 5 | && apt-get install -y fontforge \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | # Set working directory 9 | WORKDIR /app 10 | 11 | # Copy the project files 12 | COPY . . 13 | 14 | # Install Python dependencies 15 | RUN pip install -r requirements.txt 16 | 17 | # Create volume mount point for output 18 | VOLUME /app/fonts 19 | 20 | # Default build arguments 21 | ENV BUILD_ARGS="" 22 | 23 | # Run the build script with optional arguments 24 | ENTRYPOINT ["sh", "-c", "python build.py $BUILD_ARGS"] 25 | -------------------------------------------------------------------------------- /OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 The Maple Mono Project Authors (https://github.com/subframe7536/maple-font) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./source/schema.json", 3 | "family_name": "Maple Mono", 4 | "use_hinted": true, 5 | "pool_size": 4, 6 | "ligature": true, 7 | "feature_freeze": { 8 | "cv01": "ignore", 9 | "cv02": "ignore", 10 | "cv03": "ignore", 11 | "cv04": "ignore", 12 | "cv05": "ignore", 13 | "cv06": "ignore", 14 | "cv07": "ignore", 15 | "cv08": "ignore", 16 | "cv31": "ignore", 17 | "cv32": "ignore", 18 | "cv33": "ignore", 19 | "cv34": "ignore", 20 | "cv35": "ignore", 21 | "cv36": "ignore", 22 | "cv37": "ignore", 23 | "cv38": "ignore", 24 | "cv39": "ignore", 25 | "cv40": "ignore", 26 | "cv41": "ignore", 27 | "cv61": "ignore", 28 | "cv62": "ignore", 29 | "cv63": "ignore", 30 | "cv64": "ignore", 31 | "cv65": "ignore", 32 | "cv96": "ignore", 33 | "cv97": "ignore", 34 | "cv98": "ignore", 35 | "cv99": "ignore", 36 | "ss01": "ignore", 37 | "ss02": "ignore", 38 | "ss03": "ignore", 39 | "ss04": "ignore", 40 | "ss05": "ignore", 41 | "ss06": "ignore", 42 | "ss07": "ignore", 43 | "ss08": "ignore", 44 | "ss09": "ignore", 45 | "ss10": "ignore", 46 | "ss11": "ignore", 47 | "zero": "ignore" 48 | }, 49 | "nerd_font": { 50 | "enable": true, 51 | "version": "3.4.0", 52 | "mono": false, 53 | "use_font_patcher": false, 54 | "glyphs": [ 55 | "--complete" 56 | ], 57 | "extra_args": [] 58 | }, 59 | "cn": { 60 | "enable": false, 61 | "with_nerd_font": true, 62 | "fix_meta_table": true, 63 | "clean_cache": false, 64 | "narrow": false, 65 | "use_hinted": false, 66 | "use_static_base_font": true, 67 | "scale_factor": 1.0 68 | } 69 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "maple-font" 3 | version = "7.3" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "foundrytools-cli==1.1.22", 9 | "glyphslib>=6.10.1", 10 | "python-minifier>=2.11.3", 11 | "setuptools==80.0.0" 12 | ] 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv export --format requirements-txt --no-hashes --output-file requirements.txt 3 | afdko==4.0.2 4 | # via foundrytools-cli 5 | appdirs==1.4.4 6 | # via fs 7 | attrs==25.3.0 8 | # via ufolib2 9 | booleanoperations==0.9.0 10 | # via 11 | # afdko 12 | # fontparts 13 | brotli==1.1.0 14 | # via 15 | # fonttools 16 | # foundrytools-cli 17 | brotlicffi==1.1.0.0 ; platform_python_implementation != 'CPython' 18 | # via fonttools 19 | cffi==1.17.1 ; platform_python_implementation != 'CPython' 20 | # via brotlicffi 21 | cffsubr==0.3.0 22 | # via foundrytools-cli 23 | click==8.2.1 24 | # via foundrytools-cli 25 | colorama==0.4.6 ; sys_platform == 'win32' 26 | # via 27 | # click 28 | # loguru 29 | # tqdm 30 | defcon==0.12.1 31 | # via 32 | # afdko 33 | # fontparts 34 | # mutatormath 35 | # ufoprocessor 36 | dehinter==4.0.0 37 | # via foundrytools-cli 38 | fontmath==0.9.4 39 | # via 40 | # afdko 41 | # fontparts 42 | # mutatormath 43 | # ufoprocessor 44 | fontparts==0.12.7 45 | # via ufoprocessor 46 | fontpens==0.2.4 47 | # via defcon 48 | fonttools==4.58.1 49 | # via 50 | # afdko 51 | # booleanoperations 52 | # cffsubr 53 | # defcon 54 | # dehinter 55 | # fontmath 56 | # fontparts 57 | # fontpens 58 | # foundrytools-cli 59 | # glyphslib 60 | # mutatormath 61 | # ufolib2 62 | # ufoprocessor 63 | foundrytools-cli==1.1.22 64 | # via maple-font 65 | fs==2.4.16 66 | # via fonttools 67 | glyphslib==6.10.2 68 | # via maple-font 69 | loguru==0.7.3 70 | # via foundrytools-cli 71 | lxml==5.4.0 72 | # via 73 | # afdko 74 | # fonttools 75 | markdown-it-py==3.0.0 76 | # via rich 77 | mdurl==0.1.2 78 | # via markdown-it-py 79 | mutatormath==3.0.1 80 | # via ufoprocessor 81 | openstep-plist==0.5.0 82 | # via glyphslib 83 | pathvalidate==3.2.3 84 | # via foundrytools-cli 85 | pyclipper==1.3.0.post6 86 | # via booleanoperations 87 | pycparser==2.22 ; platform_python_implementation != 'CPython' 88 | # via cffi 89 | pygments==2.19.1 90 | # via rich 91 | python-minifier==2.11.3 92 | # via maple-font 93 | rich==14.0.0 94 | # via foundrytools-cli 95 | setuptools==80.0.0 96 | # via 97 | # fs 98 | # maple-font 99 | six==1.17.0 100 | # via fs 101 | skia-pathops==0.8.0.post2 102 | # via foundrytools-cli 103 | tqdm==4.67.1 104 | # via afdko 105 | ttfautohint-py==0.5.1 106 | # via foundrytools-cli 107 | typing-extensions==4.13.2 ; python_full_version < '3.11' 108 | # via rich 109 | ufolib2==0.17.1 110 | # via glyphslib 111 | ufonormalizer==0.6.2 112 | # via afdko 113 | ufoprocessor==1.13.3 114 | # via afdko 115 | unicodedata2==16.0.0 ; python_full_version < '3.13' 116 | # via fonttools 117 | win32-setctime==1.2.0 ; sys_platform == 'win32' 118 | # via loguru 119 | zopfli==0.2.3.post1 120 | # via 121 | # fonttools 122 | # foundrytools-cli 123 | -------------------------------------------------------------------------------- /resources/2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subframe7536/maple-font/2fb41712b3dd8231d1fffe48ee70c0bfb8ce2ffe/resources/2-1.png -------------------------------------------------------------------------------- /resources/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subframe7536/maple-font/2fb41712b3dd8231d1fffe48ee70c0bfb8ce2ffe/resources/header.png -------------------------------------------------------------------------------- /resources/showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subframe7536/maple-font/2fb41712b3dd8231d1fffe48ee70c0bfb8ce2ffe/resources/showcase.png -------------------------------------------------------------------------------- /source/MapleMono-Italic[wght]-VF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subframe7536/maple-font/2fb41712b3dd8231d1fffe48ee70c0bfb8ce2ffe/source/MapleMono-Italic[wght]-VF.ttf -------------------------------------------------------------------------------- /source/MapleMono-Italic[wght].vfc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subframe7536/maple-font/2fb41712b3dd8231d1fffe48ee70c0bfb8ce2ffe/source/MapleMono-Italic[wght].vfc -------------------------------------------------------------------------------- /source/MapleMono-NF-Base-Mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subframe7536/maple-font/2fb41712b3dd8231d1fffe48ee70c0bfb8ce2ffe/source/MapleMono-NF-Base-Mono.ttf -------------------------------------------------------------------------------- /source/MapleMono-NF-Base.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subframe7536/maple-font/2fb41712b3dd8231d1fffe48ee70c0bfb8ce2ffe/source/MapleMono-NF-Base.ttf -------------------------------------------------------------------------------- /source/MapleMono[wght]-VF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subframe7536/maple-font/2fb41712b3dd8231d1fffe48ee70c0bfb8ce2ffe/source/MapleMono[wght]-VF.ttf -------------------------------------------------------------------------------- /source/MapleMono[wght].vfc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subframe7536/maple-font/2fb41712b3dd8231d1fffe48ee70c0bfb8ce2ffe/source/MapleMono[wght].vfc -------------------------------------------------------------------------------- /source/cn/static.sha256: -------------------------------------------------------------------------------- 1 | 76d194a1ace4796ed3bb5aa9bfa7656813218533affc60f4f8cee521e5add52a -------------------------------------------------------------------------------- /source/config.yaml: -------------------------------------------------------------------------------- 1 | sources: 2 | - MapleMono[wght].glyphs 3 | - MapleMono-Italic[wght].glyphs 4 | axisOrder: 5 | - wght 6 | familyName: Maple Mono 7 | removeOutlineOverlaps: false 8 | stat: 9 | MapleMono[wght].ttf: 10 | - name: Weight 11 | tag: wght 12 | values: 13 | - name: Thin 14 | value: 100 15 | - name: ExtraLight 16 | value: 200 17 | - name: Light 18 | value: 300 19 | - name: Regular 20 | value: 400 21 | linkedValue: 700 22 | flags: 2 23 | - name: Medium 24 | value: 500 25 | - name: SemiBold 26 | value: 600 27 | - name: Bold 28 | value: 700 29 | - name: ExtraBold 30 | value: 800 31 | - name: Italic 32 | tag: ital 33 | values: 34 | - name: Roman 35 | value: 0 36 | linkedValue: 1 37 | flags: 2 38 | MapleMono-Italic[wght].ttf: 39 | - name: Weight 40 | tag: wght 41 | values: 42 | - name: Thin 43 | value: 100 44 | - name: ExtraLight 45 | value: 200 46 | - name: Light 47 | value: 300 48 | - name: Regular 49 | value: 400 50 | linkedValue: 700 51 | flags: 2 52 | - name: Medium 53 | value: 500 54 | - name: SemiBold 55 | value: 600 56 | - name: Bold 57 | value: 700 58 | - name: ExtraBold 59 | value: 800 60 | - name: Italic 61 | tag: ital 62 | values: 63 | - name: Italic 64 | value: 1 -------------------------------------------------------------------------------- /source/features/README.md: -------------------------------------------------------------------------------- 1 | # Ligatures And Features 2 | 3 | Here is the check list and explaination of Maple Mono ligatures and features. 4 | 5 | For more details, please check out `.fea` files in same directory and [OpenType Feature Spec](https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html). 6 | 7 | ## Usage 8 | 9 | ### VSCode 10 | 11 | Setup in your VSCode settings json file 12 | 13 | ```jsonc 14 | { 15 | // Setup font family 16 | "editor.fontFamily": "Maple Mono NF, Jetbrains Mono, Menlo, Consolas, monospace", 17 | // Enable ligatures 18 | "editor.fontLigatures": "'calt'", 19 | // Or enable OpenType features 20 | "editor.fontLigatures": "'calt', 'cv01', 'ss01', 'zero'", 21 | } 22 | ``` 23 | 24 | ### IDEA / Pycharm / WebStorm / GoLand / CLion 25 | 26 | 1. Open Settings 27 | 2. Click "Editor" 28 | 3. Click "Font" 29 | 4. Choose "Maple Mono NF" in the font menu 30 | 5. Click "Enable Ligatures" 31 | 32 | OpenType Features are not supported, you need to custom build to freeze features. 33 | 34 | ## Ligatures 35 | 36 | "Enable ligature", is same as "enable `calt` feature": 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
::;;;<!---->
:::..<->
?:...<-->
:?.?->
:?>?.<-
:=..<-->
=:.=<--
:=:<~>->
=:=~><-<
<:~~|->
:><~><-|
:<<~~-------
<:<~~>>-
>:>-~-<
::=~->-<
__~@<|||
#{~~~~~~~|||>
#[0xA12 0x56 1920x1080<||
#(<>||>
#?</<|
#!/>|>
#:</><|>
#=<+_|_
#_+>[TRACE]
#__<+>[DEBUG]
#_(<*[INFO]
]#*>[WARN]
#######<*>[ERROR]
<<>=[FATAL]
<<<<=[TODO]
>><=<[FIXME]
>>>>=>[NOTE]
{{==[HACK]
}}===[MARK]
{|!=[EROR]
|}!==[WARNING]
{{--=/=todo))
{{!--=!=fixme))
--}}|=Cl
[|<=>al
|]<==>cl
!!<==el
||==>il
??=>tl
???<=|ul
&&|=>xl
&&&=<=ff
//=>=tt
///=======all
/*>=<ell
/**\\ \' \.ill
*/--ull
++---ll
+++<!--
;;<#--
96 | 97 | 98 | ### Notice 99 | 100 | - `>>` / `>>>` is smart, but much contextual-sensitive, so it may be not effect in some IDEs ([explaination](https://github.com/subframe7536/maple-font/discussions/275)). Turn on `ss07` to force enable. 101 | 102 | ## Features 103 | 104 | ### Character Varients (cvXX) 105 | 106 | 107 | - [v7.0] cv01: Normalize special symbols (`@ $ & % Q => ->`) 108 | - [v7.0] cv02: Alternative `a` with top arm, no effect in italic style 109 | - [v7.0] cv03: Alternative `i` without left bottom bar 110 | - [v7.0] cv04: Alternative `l` with left bottom bar, like consolas, will be overrided by `cv35` in italic style 111 | - [v7.1] cv05: Alternative `g` in double story style, no effect in italic style 112 | - [v7.1] cv06: Alternative `i` without bottom bar, no effect in italic style 113 | - [v7.1] cv07: Alternative `J` without top bar, no effect in italic style 114 | - [v7.1] cv08: Alternative `r` with bottom bar, no effect in italic style 115 | - [v7.1] cv61: Alternative `,` and `;` with straight tail 116 | - [v7.1] cv62: Alternative `?` with larger openings 117 | - [v7.1] cv63: Alternative `<=` in arrow style 118 | - [v7.3] cv64: Alternative `<=` and `>=` with horizen bottom bar 119 | - [v7.3] cv65: Alternative `&` in handwriting style 120 | - [v7.0] zero: Dot style `0` 121 | 122 | 123 | #### Italic Only 124 | 125 | 126 | - [v7.0] cv31: Alternative italic `a` with top arm 127 | - [v7.0] cv32: Alternative Italic `f` without bottom tail 128 | - [v7.0] cv33: Alternative Italic `i` and `j` with left bottom bar and horizen top bar 129 | - [v7.0] cv34: Alternative Italic `k` without center circle 130 | - [v7.0] cv35: Alternative Italic `l` without center tail 131 | - [v7.0] cv36: Alternative Italic `x` without top and bottom tails 132 | - [v7.0] cv37: Alternative Italic `y` with straight intersection 133 | - [v7.1] cv38: Alternative italic `g` in double story style 134 | - [v7.1] cv39: Alternative Italic `i` without bottom bar 135 | - [v7.1] cv40: Alternative italic `J` without top bar 136 | - [v7.1] cv41: Alternative italic `r` with bottom bar 137 | 138 | 139 | #### CN Only 140 | 141 | 142 | - [v7.0] cv96: Full width quotes (`“` / `”` / `‘` / `’`) 143 | - [v7.0] cv97: Full width ellipsis (`…`) 144 | - [v7.0] cv98: Full width emdash (`—`) 145 | - [v7.0] cv99: Traditional centered punctuations 146 | 147 | 148 | ### Stylistic Sets (ssXX) 149 | 150 | 151 | - [v7.0] ss01: Broken multiple equals ligatures (`==`, `===`, `!=`, `!==` ...) 152 | - [v7.0] ss02: Broken compare and equal ligatures (`<=`, `>=`) 153 | - [v7.0] ss03: Allow to use any case in all tags 154 | - [v7.0] ss04: Broken multiple underscores ligatures (`__`, `#__`) 155 | - [v7.0] ss05: Revert thin backslash in escape symbols (`\\`, `\"`, `\.` ...) 156 | - [v7.0] ss06: Break connected strokes between italic letters (`al`, `il`, `ull` ...) 157 | - [v7.0] ss07: Relax the conditions for multiple greaters ligatures (`>>` or `>>>`) 158 | - [v7.0] ss08: Double headed arrows and reverse arrows ligatures (`>>=`, `-<<`, `->>`, `>>-` ...) 159 | - [v7.1] ss09: Asciitilde equal as not equal to ligature (`~=`) 160 | - [v7.1] ss10: Approximately equal to and approximately not equal to ligatures (`=~`, `!~`) 161 | - [v7.1] ss11: Equal and extra punctuation ligatures (`|=`, `/=`, `?=`, `&=`, ...) 162 | -------------------------------------------------------------------------------- /source/features/README_CN.md: -------------------------------------------------------------------------------- 1 | # 特性文档由代码自动生成,中文文档不再维护,请查看[英文文档](./README.md) 2 | 3 | # 连字和特性 4 | 5 | 这是一个所有连字和特性的清单和解释 6 | 7 | 查看同目录下的 `.fea` 文件 和 [OpenType Feature 语法规范](https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html) 获取更多信息。 8 | 9 | ## 使用 10 | 11 | ### VSCode 12 | 13 | 在 VSCode settings json 文件中设置 14 | 15 | ```jsonc 16 | { 17 | // 设置字体 18 | "editor.fontFamily": "Maple Mono NF CN, Menlo, Consolas, Maple UI, PingFang, 'Microsoft YaHei', monospace", 19 | // 启用连字 20 | "editor.fontLigatures": "'calt'", 21 | // 或者开启 OpenType 特性 22 | "editor.fontLigatures": "'calt', 'cv01', 'ss01', 'zero'", 23 | } 24 | ``` 25 | 26 | ### IDEA / Pycharm / WebStorm / GoLand / CLion 27 | 28 | 1. 打开设置 29 | 2. 点击 "编辑器" 30 | 3. 点击 "字体" 31 | 4. 在字体下拉框中选择 Maple Mono NF CN 32 | 5. 点击 "启用连字" 33 | 34 | 不支持 OpenType 特性,你需要自行构建以强制开启特性。 35 | 36 | ## 连字 37 | 38 | 开启连字,指启用 `calt` 特性: 39 | 40 | ``` 41 | {{ 42 | }} 43 | {{-- 44 | --}} 45 | {| 46 | |} 47 | [| 48 | |] 49 | // 50 | /// 51 | /* 52 | /** 53 | ++ 54 | +++ 55 | .? 56 | .. 57 | ... 58 | ..< 59 | 76 | -> 77 | >= 78 | <= 79 | <== 80 | !! 81 | != 82 | !== 83 | =!= 84 | => 85 | == 86 | =:= 87 | :=: 88 | := 89 | :> 90 | :< 91 | :: 92 | ;; 93 | ;;; 94 | :? 95 | :?> 96 | ::= 97 | ||- 98 | ||= 99 | |- 100 | |= 101 | || 102 | -- 103 | --- 104 | <-- 105 | ?? 106 | ??? 107 | ?: 108 | ?. 109 | && 110 | __ 111 | =/= 112 | <-< 113 | <=< 114 | <==> 115 | ==> 116 | >=> 117 | <-| 118 | <=| 119 | |=> 120 | <~ 121 | ~~ 122 | <~> 123 | <~~ 124 | -~ 125 | ~~> 126 | ~> 127 | ~- 128 | ~@ 129 | <+> 130 | <+ 131 | +> 132 | <*> 133 | <* 134 | *> 135 | 136 | 138 | << 139 | <<< 140 | >> 141 | >>> 142 | #{ 143 | #[ 144 | #( 145 | #? 146 | #_ 147 | #__ 148 | #: 149 | #= 150 | #_( 151 | ]# 152 | 0x12 153 | [TRACE] 154 | [DEBUG] 155 | [INFO] 156 | [WARN] 157 | [ERROR] 158 | [FATAL] 159 | [TODO] 160 | todo)) 161 | [FIXME] 162 | fixme)) 163 | ######## 164 | 165 | \\ \/ \" 166 | ``` 167 | 168 | ### 注意 169 | 170 | - `>>` / `>>>` 十分智能,但是和上下文强关联,因此在一些 IDE 中可能会无法生效([解释](https://github.com/subframe7536/maple-font/discussions/275))。开启 `ss07` 强制开启 171 | 172 | ## 特性 173 | 174 | ### 字符变体 (cvXX) 175 | 176 | - zero: `0` 中间带点 177 | - cv01: `@ $ & % Q => ->` 去除间隙 178 | - cv02: `a` 顶部带有横杠,对斜体 `a` 无影响 179 | - cv03: `i` 去除底部横杠 180 | - cv04: `l` 底部带有横杠, 和 consolas 一样, 在斜体中将被 `cv35` 覆盖 181 | 182 | #### 斜体独占 183 | - cv31: 斜体 `a` 顶部带有横杠 184 | - cv32: 斜体 `f` 去除底部尾巴,和常规体一样 185 | - cv33: 斜体 `i j` 带有底部横杠和水平的顶部横杠,和常规体一样 186 | - cv34: 斜体 `k` 去除中间的圆圈,和常规体一样 187 | - cv35: 斜体 `l` 去除中间的尾巴,和常规体一样 188 | - cv36: 斜体 `x` 去除顶部和底部的尾巴,和常规体一样 189 | - cv37: 斜体 `y` 中间笔直的交叉,和常规体一样 190 | 191 | #### 中文独占 192 | 193 | - cv96: 全宽的 `“`(前双引号), `”`(后双引号), `‘`(前单引号), `’`(后单引号) 194 | - cv97: 全宽的 `…`(省略号) 195 | - cv98: 全宽的 `—`(破折号) 196 | - cv99: 繁体的标点(居中) 197 | 198 | ### 样式集 (ssXX) 199 | 200 | - ss01: 分离的等号连字 (`==`, `===`, `!=`, `!==`, `=/=`) 201 | - ss02: 分离的比较连字 (`<=`, `>=`) 202 | - ss03: 启用任意的纯文本标签 (在标签中支持使用任意大小写字母) 203 | - ss04: 分离的多下划线连字 (`__`, `#__`) 204 | - ss05: 在转义字符中显示正常粗细的转义符号 (`\\`, `\"`, `\,` ...) 205 | - ss06: 去除斜体的连笔 (`al`, `ul`, `il` ...) 206 | - ss07: 放宽启用多个大于号连字的条件 (`>>` or `>>>`) 207 | - ss08: 启用双箭头和反向箭头连字 (`>>=`, `-<<`, `->>`, `>-` ...) 208 | -------------------------------------------------------------------------------- /source/features/cn.fea: -------------------------------------------------------------------------------- 1 | # Auto generated by `python task.py fea` 2 | 3 | # Centered punctuations 4 | lookup PunctuationTW { 5 | sub uni3001 by uni3001.tw; 6 | sub uni3002 by uni3002.tw; 7 | sub uniFF01 by uniFF01.tw; 8 | sub uniFF0C by uniFF0C.tw; 9 | sub uniFF1A by uniFF1A.tw; 10 | sub uniFF1B by uniFF1B.tw; 11 | sub uniFF1F by uniFF1F.tw; 12 | } PunctuationTW; 13 | 14 | feature locl { 15 | 16 | language ZHH; 17 | lookup PunctuationTW; 18 | language ZHT; 19 | lookup PunctuationTW; 20 | 21 | } locl; 22 | 23 | feature ccmp { 24 | 25 | lookup ccmp_jp { 26 | sub uni3042 uni3042 by uni30423099; 27 | sub uni3044 uni3044 by uni30443099; 28 | sub uni3048 uni3048 by uni30483099; 29 | sub uni304A uni304A by uni304A3099; 30 | sub uni304B uni304B by uni304B309A; 31 | sub uni304D uni304D by uni304D309A; 32 | sub uni304F uni304F by uni304F309A; 33 | sub uni3051 uni3051 by uni3051309A; 34 | sub uni3053 uni3053 by uni3053309A; 35 | sub uni3093 uni3093 by uni30933099; 36 | sub uni30A2 uni30A2 by uni30A23099; 37 | sub uni30A4 uni30A4 by uni30A43099; 38 | sub uni30A8 uni30A8 by uni30A83099; 39 | sub uni30AA uni30AA by uni30AA3099; 40 | sub uni30AB uni30AB by uni30AB309A; 41 | sub uni30AD uni30AD by uni30AD309A; 42 | sub uni30AF uni30AF by uni30AF309A; 43 | sub uni30B1 uni30B1 by uni30B1309A; 44 | sub uni30B3 uni30B3 by uni30B3309A; 45 | sub uni30BB uni30BB by uni30BB309A; 46 | sub uni30C4 uni30C4 by uni30C4309A; 47 | sub uni30C8 uni30C8 by uni30C8309A; 48 | sub uni30F3 uni30F3 by uni30F33099; 49 | } ccmp_jp; 50 | 51 | } ccmp; 52 | 53 | feature cv96 { 54 | 55 | cvParameters { 56 | FeatUILabelNameID { 57 | name "CV96: Full width quotes"; 58 | }; 59 | }; 60 | 61 | sub quotedblleft by quotedblleft.full; 62 | sub quotedblright by quotedblright.full; 63 | sub quoteleft by quoteleft.full; 64 | sub quoteright by quoteright.full; 65 | 66 | } cv96; 67 | 68 | feature cv97 { 69 | 70 | cvParameters { 71 | FeatUILabelNameID { 72 | name "CV97: Full width ellipsis"; 73 | }; 74 | }; 75 | 76 | sub ellipsis by ellipsis.full; 77 | 78 | } cv97; 79 | 80 | feature cv98 { 81 | 82 | cvParameters { 83 | FeatUILabelNameID { 84 | name "CV98: Full width emdash"; 85 | }; 86 | }; 87 | 88 | sub emdash by emdash.full; 89 | 90 | } cv98; 91 | 92 | feature cv99 { 93 | 94 | cvParameters { 95 | FeatUILabelNameID { 96 | name "CV99: Traditional centered punctuations"; 97 | }; 98 | }; 99 | 100 | lookup PunctuationTW; 101 | 102 | } cv99; -------------------------------------------------------------------------------- /source/py/feature/README.md: -------------------------------------------------------------------------------- 1 | # Maple Mono Feature Module 2 | 3 | This module provides utilities for defining and managing OpenType font features. It includes tools for creating stylistic sets, character variants, ligatures, and more. 4 | 5 | ## Overview 6 | 7 | The `feature/` module is designed to simplify the creation of OpenType font features. It uses an abstract syntax tree (AST) approach to define and manage features programmatically. 8 | 9 | ### Key Components 10 | 11 | - **`ast.py`**: Core utilities for defining OpenType features. 12 | - **`regular.py`**: Entry file for regular features. 13 | - **`italic.py`**: Entry file for italic features. 14 | - **`base/`**: Foundational classes and features (e.g., numbers, cases, localized forms). 15 | - **`calt/`**: Default ligatures. 16 | - **`cv/`**: Character variants. 17 | - **`ss/`**: Stylistic sets. 18 | 19 | ## Usage 20 | 21 | ### Custom Tags 22 | 23 | The `calt/tag.py` file provides utilities for creating custom tags. 24 | 25 | There are many built-in tags with full-round border in the font, you can use `subst_liga` function to custom trigger text. Following example shows how to convert `TODO:` to `(TODO)` (Cons: the first letter before the tag will be overlapped) 26 | 27 | ```py 28 | subst_liga( 29 | source="TODO:", 30 | target="tag_todo.liga", 31 | lookup_name="todo_colon" 32 | ) 33 | ``` 34 | 35 | If you want to get more tags, use `tag_custom` function: 36 | 37 | ```py 38 | tag_custom( 39 | [ 40 | (":attention:", "[attention]"), 41 | ("_noqa_", "(noqa)"), 42 | # ("_alter_", ""), 43 | ], 44 | bg_cls_dict, 45 | ) 46 | ``` 47 | This converts: 48 | ``` 49 | :attention: _noqa_ 50 | ``` 51 | 52 | into a styled tag: 53 | 54 | ![Image](https://github.com/user-attachments/assets/e67f282c-e961-4e55-9169-2f20d7ccfbc6) 55 | 56 | #### Limitations 57 | 58 | 1. Built-in tags are optimized for spacing; custom tags are not. 59 | 2. Tags may split if letter spacing > 0. See [#381](https://github.com/subframe7536/maple-font/issues/381#issuecomment-2808022878). 60 | 3. Tags inherit the original text color. See [#381](https://github.com/subframe7536/maple-font/issues/381#issuecomment-2809622541). 61 | 62 | ### Remove Infinite Ligatures 63 | 64 | Like Fira Code, glyphs of multiple `=` / `-` / `~` / `#` will be combined into one glyphs in default ligature. If you don't want these ligatures, please setup `__USE_INFINITE = False` in [calt/_infinite_utils.py](./calt/_infinite_utils.py) and rebuild. 65 | 66 | ## Freeze Feature in Variable Format 67 | 68 | There are two strategies to freeze a feature: 69 | 70 | 1. For lookups implementing new ligatures (e.g., `ss08`), move rules into `calt`. 71 | 2. For glyph replacements (e.g., `cv01`), substitute glyphs directly with their originals. 72 | 73 | Currently, the second approach cannot be implemented in variable format, so in the V7.0 release all variable format variants are identical except for the family name. The build script provides an escape hatch with the `--apply-fea-file` flag. 74 | 75 | Since the feature-loading logic was refactored to Python, features are loaded dynamically. The logic in [`common.py`](./common.py) moves rules from the target feature to `calt` (method 1). Enabling the `calt` feature will yield styling equivalent to the static version. 76 | 77 | So, please **ENABLE FONT LIGATURE** to make all features work if you are using variable font (NOT recommended). 78 | 79 | ## AST Utilities 80 | 81 | The `ast.py` file provides classes and functions to define OpenType features. Below are some key utilities: 82 | 83 | ### `Clazz` 84 | 85 | Represents a class of glyphs. 86 | 87 | ```py 88 | from source.py.feature.ast import Clazz, subst 89 | 90 | cls_digit = Clazz("Digit", ["zero", "one", "two", "three"]) 91 | cls_digit.state() 92 | subst(cls_digit.use(), "a", "b", "c") 93 | ``` 94 | 95 | Generated fea string: 96 | 97 | ```fea 98 | @Digit = [zero, one, two, three]; 99 | sub @Digit a' b by c; 100 | ``` 101 | 102 | ### `Lookup` 103 | 104 | Defines a lookup block for substitutions. 105 | 106 | ```py 107 | from source.py.feature.ast import Lookup, subst 108 | 109 | lookup_example = Lookup( 110 | name="example_lookup", 111 | desc="Example substitution", 112 | content=[ 113 | subst("a", "b", None, "c"), 114 | ], 115 | ) 116 | ``` 117 | 118 | Generated fea string: 119 | 120 | ```fea 121 | # Example substitution 122 | lookup example_lookup { 123 | sub a b' by c; 124 | } example_lookup; 125 | ``` 126 | 127 | ### `Feature` 128 | 129 | Represents an OpenType feature. 130 | 131 | ```py 132 | from source.py.feature.ast import Feature 133 | 134 | feature_example = Feature( 135 | tag="calt", 136 | content=[ 137 | lookup_example, 138 | ], 139 | ) 140 | ``` 141 | 142 | Generated fea string: 143 | 144 | ```fea 145 | feature calt { 146 | 147 | # Example substitution 148 | lookup example_lookup { 149 | sub a b' by c; 150 | } example_lookup; 151 | 152 | } 153 | ``` 154 | 155 | ### Create 156 | 157 | Generates the final OpenType feature file content. 158 | 159 | ```py 160 | from source.py.feature.ast import create 161 | 162 | fea_content = create([feature_example]) 163 | print(fea_content) 164 | ``` 165 | 166 | ## Generating Features 167 | 168 | In most of time, you don't need to update the fea files. The generated fea string will be automatically applied at build time without using `--apply-fea-file` flag. 169 | 170 | To update existing fea files, you can run: 171 | 172 | ```sh 173 | uv run task.py fea 174 | ``` 175 | 176 | Here is an example to show how to use the `generate_fea_string` function to generate feature files 177 | 178 | ```py 179 | from source.py.feature import generate_fea_string 180 | 181 | fea_string = generate_fea_string(italic=False, cn=True) 182 | print(fea_string) 183 | ``` 184 | -------------------------------------------------------------------------------- /source/py/feature/__init__.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from html import escape 3 | import json 4 | from source.py.feature import ast 5 | from source.py.feature.base import get_base_feature_cn_only, get_base_features 6 | from source.py.feature.base.lang import get_lang_list 7 | from source.py.feature.calt import get_calt, get_calt_lookup 8 | from source.py.feature.cv import cv96, cv97, cv98, cv99 9 | from source.py.feature.regular import ( 10 | cls_var, 11 | cls_hex_letter, 12 | class_list_regular, 13 | cv_list_regular, 14 | ss_list_regular, 15 | ) 16 | from source.py.feature.italic import ( 17 | class_list_italic, 18 | cv_list_italic, 19 | ss_list_italic, 20 | ) 21 | 22 | normal_enabled_features = [ 23 | "cv01", 24 | "cv02", 25 | "cv33", 26 | "cv34", 27 | "cv35", 28 | "cv36", 29 | "cv61", 30 | "cv62", 31 | "ss05", 32 | "ss06", 33 | "ss07", 34 | "ss08", 35 | ] 36 | 37 | 38 | cv_list_cn = [ 39 | cv96.cv96_feat_cn, 40 | cv97.cv97_feat_cn, 41 | cv98.cv98_feat_cn, 42 | cv99.cv99_feat_cn, 43 | ] 44 | 45 | 46 | def generate_fea_string( 47 | is_italic: bool, 48 | is_cn: bool, 49 | is_normal: bool = False, 50 | is_calt: bool = True, 51 | variable_enabled_feature_list: list[str] | None = None, 52 | ): 53 | """ 54 | Generates feature string. 55 | 56 | For ``variable=True, normal=True``, enabled features are 57 | moved to calt feature to freeze them. 58 | 59 | Args: 60 | is_italic (bool): Whether to generate italic features 61 | is_cn (bool): Whether to include Chinese-specific features 62 | is_normal (bool): Whether to generate normal preset 63 | is_calt (bool): Whether to enable calt 64 | variable_enabled_feature_list (list[str]): List of features that 65 | be enabled in variable format 66 | """ 67 | print( 68 | f"Generating feature string with italic={is_italic}, cn={is_cn}, normal={is_normal}, calt={is_calt}, variable={bool(variable_enabled_feature_list)}" 69 | ) 70 | 71 | class_list = class_list_italic if is_italic else class_list_regular 72 | cv_list = cv_list_italic if is_italic else cv_list_regular 73 | ss_list = ss_list_italic if is_italic else ss_list_regular 74 | 75 | if class_list[-2].name != "Var" or class_list[-1].name != "HexLetter": 76 | raise TypeError("Invalid class_list, must ends with [@Var, @HexLetter]") 77 | 78 | calt_feat = get_calt( 79 | class_list[-2], class_list[-1], is_italic=is_italic, is_normal=is_normal 80 | ) 81 | 82 | # clear calt for no ligature 83 | if not is_calt: 84 | calt_feat.content = [] 85 | 86 | cv_ss_list = deepcopy(cv_list + (cv_list_cn if is_cn else []) + ss_list) 87 | 88 | # for variable font, freeze feature by moving it to `calt` 89 | if variable_enabled_feature_list: 90 | extracted_lookup_list = [] 91 | for feat in cv_ss_list: 92 | if feat.tag in variable_enabled_feature_list or []: 93 | # prevent features that add ligatures like `ss08` 94 | if not is_calt and feat.has_lookup: 95 | continue 96 | 97 | extracted_lookup_list.append( 98 | feat.content 99 | if feat.has_lookup 100 | else [ast.Lookup(f"move_{feat.tag}", None, feat.content)] 101 | ) 102 | 103 | # cleanup 104 | feat.content = [] 105 | 106 | calt_feat.content.extend(extracted_lookup_list) 107 | 108 | # remove calt if empty, to prevent fonttools warning 109 | if not calt_feat.content: 110 | calt_feat = None 111 | 112 | return ast.create( 113 | [ 114 | class_list, 115 | get_lang_list(), 116 | get_base_features(calt_feat, is_cn=is_cn), 117 | cv_ss_list, 118 | ], 119 | ) 120 | 121 | 122 | def generate_fea_string_cn_only(): 123 | return ast.create( 124 | [ 125 | get_base_feature_cn_only(), 126 | cv_list_cn, 127 | ], 128 | ) 129 | 130 | 131 | def get_all_calt_text(): 132 | result: list[str] = [] 133 | 134 | for item in ast.recursive_iterate(get_calt_lookup(cls_var, cls_hex_letter, True)): 135 | if isinstance(item, ast.Lookup) and item.desc: 136 | if item.name == "escape": 137 | result.append(item.desc.replace("\\ ", "\\\\ ")) 138 | elif item.name.startswith('infinite'): 139 | result.extend(item.desc.split(' ')) 140 | elif not item.name.endswith("__ALT__"): 141 | result.append(item.desc) 142 | 143 | # Split into three columns 144 | third = (len(result) + 2) // 3 # Round up for numbers not divisible by 3 145 | 146 | # Create HTML table with three equal columns 147 | html_rows = [""] 148 | 149 | def wrap(str): 150 | return f"" if str else "" 151 | 152 | for i in range(third): 153 | col1 = wrap(result[i]) 154 | col2 = wrap(result[i + third] if i + third < len(result) else "") 155 | col3 = wrap(result[i + 2 * third] if i + 2 * third < len(result) else "") 156 | html_rows.append(f"{col1}{col2}{col3}") 157 | 158 | html_rows.append("
{escape(str)}
") 159 | return "\n".join(html_rows) 160 | 161 | 162 | zero_desc = "Dot style `0`" 163 | 164 | 165 | def get_version_info( 166 | features: list[ast.CharacterVariant] | list[ast.StylisticSet], 167 | ) -> dict[str, dict[str, str]]: 168 | result = {} 169 | for item in features: 170 | if item.version not in result: 171 | result[item.version] = {} 172 | result[item.version][item.tag] = item.sample 173 | return result 174 | 175 | 176 | def get_cv_desc(): 177 | return "\n".join( 178 | [cv.desc_item() for cv in cv_list_regular] + [f"- [v7.0] zero: {zero_desc}"] 179 | ) 180 | 181 | 182 | def get_cv_version_info() -> dict[str, dict[str, str]]: 183 | return get_version_info(cv_list_regular) 184 | 185 | 186 | def get_cv_italic_desc(): 187 | return "\n".join( 188 | [cv.desc_item() for cv in cv_list_italic if cv.id > 30 and cv.id < 61] 189 | ) 190 | 191 | 192 | def get_cv_italic_version_info() -> dict[str, dict[str, str]]: 193 | return get_version_info([cv for cv in cv_list_italic if cv.id > 30 and cv.id < 61]) 194 | 195 | 196 | def get_cv_cn_desc(): 197 | return "\n".join([cv.desc_item() for cv in cv_list_cn]) 198 | 199 | 200 | def get_cv_cn_version_info() -> dict[str, dict[str, str]]: 201 | return get_version_info(cv_list_cn) 202 | 203 | 204 | def get_ss_desc(): 205 | result = {} 206 | for ss in ss_list_regular + ss_list_italic: 207 | if ss.id not in result: 208 | desc = ss.desc_item() 209 | 210 | if ss.id == 5: 211 | desc = desc.replace("`\\\\`", "`\\\\\\\\`") 212 | 213 | result[ss.id] = desc 214 | 215 | return "\n".join(sorted(result.values())) 216 | 217 | 218 | def get_ss_version_info() -> dict[str, dict[str, str]]: 219 | ss = list({s.tag: s for s in ss_list_regular + ss_list_italic}.values()) 220 | return get_version_info(sorted(ss, key=lambda x: x.tag)) 221 | 222 | 223 | __total_feat_list = ( 224 | cv_list_regular + cv_list_italic + cv_list_cn + ss_list_regular + ss_list_italic 225 | ) 226 | 227 | 228 | def get_total_feat_dict() -> dict[str, str]: 229 | result = {} 230 | 231 | for item in __total_feat_list: 232 | if item.tag not in result: 233 | result[item.tag] = f"[v{item.version}] " + item.desc.replace("`", "'") 234 | 235 | result["zero"] = "[v7.0] " + zero_desc.replace("`", "'") 236 | 237 | return dict(sorted(result.items())) 238 | 239 | 240 | def get_total_feat_ts() -> str: 241 | feat_dict = {} 242 | 243 | for item in __total_feat_list: 244 | if item.tag not in feat_dict: 245 | feat_dict[item.tag] = item.desc 246 | 247 | feat_dict["calt"] = "Default ligatures" 248 | feat_dict["zero"] = zero_desc 249 | 250 | feat_dict = dict(sorted(feat_dict.items())) 251 | 252 | js_object = "\n" 253 | for key, val in feat_dict.items(): 254 | js_object += f" /** {val} */\n {key}: string\n" 255 | 256 | return f"""// Auto generated by `python task.py fea` 257 | // @prettier-ignore 258 | /* eslint-disable */ 259 | 260 | export interface FeatureDescription {{{js_object}}} 261 | 262 | export const featureArray = {json.dumps(list(feat_dict.keys()), indent=2)} 263 | 264 | export const normalFeatureArray = {json.dumps(normal_enabled_features, indent=2)} 265 | """ 266 | 267 | 268 | def get_freeze_moving_rules() -> list[str]: 269 | result = set() 270 | 271 | for feat in __total_feat_list: 272 | if feat.has_lookup: 273 | result.add(feat.tag) 274 | 275 | return list(result) 276 | -------------------------------------------------------------------------------- /source/py/feature/base/__init__.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.base.case import get_case_feature 3 | from source.py.feature.base.ccmp import get_ccmp_feature 4 | from source.py.feature.base.number import get_number_feature_list 5 | from source.py.feature.base.locl import get_locl_feature_list 6 | 7 | 8 | def get_base_features(calt: ast.Feature | None, is_cn: bool): 9 | aalt_feat_list = ( 10 | get_locl_feature_list(cn=is_cn) 11 | + [get_case_feature()] 12 | + get_number_feature_list() 13 | + [calt] 14 | ) 15 | 16 | aalt_feature = ast.Feature( 17 | "aalt", 18 | [feat.use() for feat in aalt_feat_list if isinstance(feat, ast.Feature)], 19 | "7.0", 20 | ) 21 | 22 | return [aalt_feature, get_ccmp_feature(cn=is_cn)] + aalt_feat_list 23 | 24 | 25 | def get_base_feature_cn_only(): 26 | result = get_locl_feature_list(cn=True, cn_only=True) 27 | result.append(get_ccmp_feature(cn=True, cn_only=True)) 28 | return result 29 | -------------------------------------------------------------------------------- /source/py/feature/base/case.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | case_glyphs = [ 5 | "colon", 6 | "periodcentered.loclCAT", 7 | "dieresiscomb", 8 | "dotaccentcomb", 9 | "gravecomb", 10 | "acutecomb", 11 | "hungarumlautcomb", 12 | "circumflexcomb", 13 | "caroncomb", 14 | "brevecomb", 15 | "ringcomb", 16 | "tildecomb", 17 | "macroncomb", 18 | "hookabovecomb", 19 | "dblgravecomb", 20 | "commaturnedabovecomb", 21 | "horncomb", 22 | "dotbelowcomb", 23 | "commaaccentcomb", 24 | "cedillacomb", 25 | "ogonekcomb", 26 | "dieresis", 27 | "dotaccent", 28 | "grave", 29 | "acute", 30 | "hungarumlaut", 31 | "circumflex", 32 | "caron", 33 | "breve", 34 | "ring", 35 | "tilde", 36 | "macron", 37 | "tonos", 38 | "brevecomb_acutecomb", 39 | "brevecomb_gravecomb", 40 | "brevecomb_hookabovecomb", 41 | "brevecomb_tildecomb", 42 | "circumflexcomb_acutecomb", 43 | "circumflexcomb_gravecomb", 44 | "circumflexcomb_hookabovecomb", 45 | "circumflexcomb_tildecomb", 46 | ] 47 | 48 | 49 | def get_case_feature(): 50 | return ast.Feature( 51 | "case", 52 | ast.subst_map( 53 | case_glyphs, 54 | target_suffix=".case", 55 | ), 56 | "7.0", 57 | ) 58 | -------------------------------------------------------------------------------- /source/py/feature/base/ccmp.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.base.clazz import cls_uppercase 3 | 4 | 5 | comb_top_acc = ast.Clazz( 6 | "CombiningTopAccents", 7 | [ 8 | "acutecomb", 9 | "brevecomb", 10 | "caroncomb", 11 | "circumflexcomb", 12 | "commaturnedabovecomb", 13 | "dblgravecomb", 14 | "dieresiscomb", 15 | "dotaccentcomb", 16 | "gravecomb", 17 | "hookabovecomb", 18 | "hungarumlautcomb", 19 | "macroncomb", 20 | "ringcomb", 21 | "tildecomb", 22 | ], 23 | ) 24 | 25 | comb_non_top_acc = ast.Clazz( 26 | "CombiningNonTopAccents", 27 | [ 28 | "cedillacomb", 29 | "dotbelowcomb", 30 | "ogonekcomb", 31 | "ringbelowcomb", 32 | "horncomb", 33 | "slashlongcomb", 34 | "slashshortcomb", 35 | "strokelongcomb", 36 | ], 37 | ) 38 | 39 | marks = [ 40 | "dieresiscomb", 41 | "dotaccentcomb", 42 | "gravecomb", 43 | "acutecomb", 44 | "hungarumlautcomb", 45 | "circumflexcomb", 46 | "caroncomb", 47 | "brevecomb", 48 | "ringcomb", 49 | "tildecomb", 50 | "macroncomb", 51 | "hookabovecomb", 52 | "dblgravecomb", 53 | "commaturnedabovecomb", 54 | "horncomb", 55 | "dotbelowcomb", 56 | "commaaccentcomb", 57 | "cedillacomb", 58 | "ogonekcomb", 59 | "dieresis", 60 | "dotaccent", 61 | "acute", 62 | "hungarumlaut", 63 | "circumflex", 64 | "caron", 65 | "breve", 66 | "ring", 67 | "tilde", 68 | "macron", 69 | "tonos", 70 | "brevecomb_acutecomb", 71 | "brevecomb_gravecomb", 72 | "brevecomb_hookabovecomb", 73 | "brevecomb_tildecomb", 74 | "circumflexcomb_acutecomb", 75 | "circumflexcomb_gravecomb", 76 | "circumflexcomb_hookabovecomb", 77 | "circumflexcomb_tildecomb", 78 | ] 79 | 80 | marks_comb = ast.Clazz("Markscomb", marks) 81 | marks_comb_case = ast.Clazz("MarkscombCase", [f"{m}.case" for m in marks]) 82 | 83 | 84 | def comb(c1: str, c2: str) -> list[ast.Line]: 85 | return [ 86 | ast.__subst(f"{c1}comb {c2}comb", f"{c1}comb_{c2}comb"), 87 | ast.__subst(f"{c1}comb.case {c2}comb.case", f"{c1}comb_{c2}comb.case"), 88 | ] 89 | 90 | 91 | def comb_jp(c1: str, c2: str) -> ast.Line: 92 | return ast.__subst(f"uni{c1} uni{c1}", f"uni{c1}{c2}") 93 | 94 | 95 | ccmp_latn = ast.Lookup( 96 | "ccmp_latn", 97 | None, 98 | [ 99 | ast.Line("lookupflag 0;"), 100 | comb("breve", "acute"), 101 | comb("breve", "grave"), 102 | comb("breve", "hookabove"), 103 | comb("breve", "tilde"), 104 | comb("circumflex", "acute"), 105 | comb("circumflex", "grave"), 106 | comb("circumflex", "hookabove"), 107 | comb("circumflex", "tilde"), 108 | ], 109 | ) 110 | start_other = ast.cls("i", "i-cy", "iogonek", "idotbelow", "j", "je-cy") 111 | end_other = ast.cls( 112 | "idotless", 113 | "idotless", 114 | "iogonekdotless", 115 | "idotbelowdotless", 116 | "jdotless", 117 | "jdotless", 118 | ) 119 | 120 | ccmp_other_name = "ccmp_other" 121 | ccmp_other = ast.Lookup( 122 | ccmp_other_name, 123 | None, 124 | [ 125 | ast.subst( 126 | None, 127 | start_other, 128 | comb_top_acc, 129 | end_other, 130 | ), 131 | ast.subst( 132 | None, 133 | start_other, 134 | [comb_non_top_acc, comb_top_acc], 135 | end_other, 136 | ), 137 | ast.subst(marks_comb, marks_comb, None, marks_comb_case), 138 | ast.subst(cls_uppercase, marks_comb, None, marks_comb_case), 139 | ast.subst(None, marks_comb, marks_comb_case, marks_comb_case), 140 | ast.subst(marks_comb_case, marks_comb, None, marks_comb_case), 141 | ], 142 | ) 143 | 144 | ccmp_jp = ast.Lookup( 145 | "ccmp_jp", 146 | None, 147 | [ 148 | comb_jp("3042", "3099"), 149 | comb_jp("3044", "3099"), 150 | comb_jp("3048", "3099"), 151 | comb_jp("304A", "3099"), 152 | comb_jp("304B", "309A"), 153 | comb_jp("304D", "309A"), 154 | comb_jp("304F", "309A"), 155 | comb_jp("3051", "309A"), 156 | comb_jp("3053", "309A"), 157 | comb_jp("3093", "3099"), 158 | comb_jp("30A2", "3099"), 159 | comb_jp("30A4", "3099"), 160 | comb_jp("30A8", "3099"), 161 | comb_jp("30AA", "3099"), 162 | comb_jp("30AB", "309A"), 163 | comb_jp("30AD", "309A"), 164 | comb_jp("30AF", "309A"), 165 | comb_jp("30B1", "309A"), 166 | comb_jp("30B3", "309A"), 167 | comb_jp("30BB", "309A"), 168 | comb_jp("30C4", "309A"), 169 | comb_jp("30C8", "309A"), 170 | comb_jp("30F3", "3099"), 171 | ], 172 | ) 173 | 174 | __ccmp = [ 175 | ast.cls_states( 176 | comb_top_acc, 177 | comb_non_top_acc, 178 | marks_comb, 179 | marks_comb_case, 180 | ), 181 | ccmp_other, 182 | ccmp_latn, 183 | ast.script("latn"), 184 | ccmp_other.use(), 185 | ] 186 | 187 | def get_ccmp_feature(cn: bool, cn_only: bool = False): 188 | if cn: 189 | content = ccmp_jp if cn_only else [__ccmp, ccmp_jp] 190 | else: 191 | content = __ccmp 192 | 193 | return ast.Feature("ccmp", content, "7.0") -------------------------------------------------------------------------------- /source/py/feature/base/clazz.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | cls_zero = ast.Clazz("Zero", ["zero", "zero.zero"]) 4 | cls_one = ast.Clazz("One", ["one", "one.cv04"]) 5 | cls_digit = ast.Clazz( 6 | "Digit", 7 | [ 8 | cls_zero, 9 | cls_one, 10 | "two", 11 | "three", 12 | "four", 13 | "five", 14 | "six", 15 | "seven", 16 | "eight", 17 | "nine", 18 | ], 19 | ) 20 | cls_space = ast.Clazz("Space", ["space", "nbspace"]) 21 | cls_normal_separator = ast.Clazz( 22 | "NormalSeparator", 23 | [ 24 | "{", 25 | "}", 26 | "[", 27 | "]", 28 | "(", 29 | ")", 30 | "|", 31 | "/", 32 | "\\", 33 | ], 34 | ) 35 | cls_comma = ast.Clazz("Comma", [",", ast.gly(",", ".cv61")]) 36 | cls_question = ast.Clazz("Question", ["?", ast.gly("?", ".cv62")]) 37 | 38 | cls_uppercase = ast.Clazz( 39 | "Uppercase", 40 | [ 41 | "A", 42 | "Aacute", 43 | "Abreve", 44 | "Abreveacute", 45 | "Abrevedotbelow", 46 | "Abrevegrave", 47 | "Abrevehookabove", 48 | "Abrevetilde", 49 | "Acaron", 50 | "Acircumflex", 51 | "Acircumflexacute", 52 | "Acircumflexdotbelow", 53 | "Acircumflexgrave", 54 | "Acircumflexhookabove", 55 | "Acircumflextilde", 56 | "Adieresis", 57 | "Adotbelow", 58 | "Agrave", 59 | "Ahookabove", 60 | "Amacron", 61 | "Aogonek", 62 | "Aring", 63 | "Atilde", 64 | "AE", 65 | "AEacute", 66 | "B", 67 | "C", 68 | "Cacute", 69 | "Ccaron", 70 | "Ccedilla", 71 | "Ccircumflex", 72 | "Cdotaccent", 73 | "D", 74 | "Eth", 75 | "Dcaron", 76 | "Dcroat", 77 | "E", 78 | "Eacute", 79 | "Ebreve", 80 | "Ecaron", 81 | "Ecircumflex", 82 | "Ecircumflexacute", 83 | "Ecircumflexdotbelow", 84 | "Ecircumflexgrave", 85 | "Ecircumflexhookabove", 86 | "Ecircumflextilde", 87 | "Edieresis", 88 | "Edotaccent", 89 | "Edotbelow", 90 | "Egrave", 91 | "Ehookabove", 92 | "Emacron", 93 | "Eogonek", 94 | "Eopen", 95 | "Etilde", 96 | "Schwa", 97 | "F", 98 | "G", 99 | "Gacute", 100 | "Gbreve", 101 | "Gcaron", 102 | "Gcircumflex", 103 | "Gcommaaccent", 104 | "Gdotaccent", 105 | "H", 106 | "Hbar", 107 | "Hcircumflex", 108 | "I", 109 | "IJ", 110 | "IJ_acute", 111 | "Iacute", 112 | "Ibreve", 113 | "Icircumflex", 114 | "Idieresis", 115 | "Idotaccent", 116 | "Idotbelow", 117 | "Igrave", 118 | "Ihookabove", 119 | "Imacron", 120 | "Iogonek", 121 | "Itilde", 122 | "J", 123 | "Jcircumflex", 124 | "K", 125 | "Kcommaaccent", 126 | "L", 127 | "Lacute", 128 | "Lcaron", 129 | "Lcommaaccent", 130 | "Ldot", 131 | "Lslash", 132 | "M", 133 | "N", 134 | "Nacute", 135 | "Ncaron", 136 | "Ncommaaccent", 137 | "Ntilde", 138 | "Eng", 139 | "O", 140 | "Oacute", 141 | "Obreve", 142 | "Ocircumflex", 143 | "Ocircumflexacute", 144 | "Ocircumflexdotbelow", 145 | "Ocircumflexgrave", 146 | "Ocircumflexhookabove", 147 | "Ocircumflextilde", 148 | "Odieresis", 149 | "Odotbelow", 150 | "Ograve", 151 | "Ohookabove", 152 | "Ohorn", 153 | "Ohornacute", 154 | "Ohorndotbelow", 155 | "Ohorngrave", 156 | "Ohornhookabove", 157 | "Ohorntilde", 158 | "Ohungarumlaut", 159 | "Omacron", 160 | "Oogonek", 161 | "Oslash", 162 | "Oslashacute", 163 | "Otilde", 164 | "OE", 165 | "P", 166 | "Thorn", 167 | "Q", 168 | "R", 169 | "Racute", 170 | "Rcaron", 171 | "Rcommaaccent", 172 | "S", 173 | "Sacute", 174 | "Scaron", 175 | "Scedilla", 176 | "Scircumflex", 177 | "Scommaaccent", 178 | "Germandbls", 179 | "T", 180 | "Tbar", 181 | "Tcaron", 182 | "Tcedilla", 183 | "Tcommaaccent", 184 | "U", 185 | "Uacute", 186 | "Ubreve", 187 | "Ucircumflex", 188 | "Udieresis", 189 | "Udotbelow", 190 | "Ugrave", 191 | "Uhookabove", 192 | "Uhorn", 193 | "Uhornacute", 194 | "Uhorndotbelow", 195 | "Uhorngrave", 196 | "Uhornhookabove", 197 | "Uhorntilde", 198 | "Uhungarumlaut", 199 | "Umacron", 200 | "Uogonek", 201 | "Uring", 202 | "Utilde", 203 | "V", 204 | "W", 205 | "Wacute", 206 | "Wcircumflex", 207 | "Wdieresis", 208 | "Wgrave", 209 | "X", 210 | "Y", 211 | "Yacute", 212 | "Ycircumflex", 213 | "Ydieresis", 214 | "Ydotbelow", 215 | "Ygrave", 216 | "Yhookabove", 217 | "Ymacron", 218 | "Ytilde", 219 | "Z", 220 | "Zacute", 221 | "Zcaron", 222 | "Zdotaccent", 223 | "A-cy", 224 | "Be-cy", 225 | "Ve-cy", 226 | "Ge-cy", 227 | "Gje-cy", 228 | "Gheupturn-cy", 229 | "Ghestroke-cy", 230 | "De-cy", 231 | "Ie-cy", 232 | "Io-cy", 233 | "Zhe-cy", 234 | "Ze-cy", 235 | "Ii-cy", 236 | "Iishort-cy", 237 | "Ka-cy", 238 | "Kje-cy", 239 | "El-cy", 240 | "Em-cy", 241 | "En-cy", 242 | "O-cy", 243 | "Pe-cy", 244 | "Er-cy", 245 | "Es-cy", 246 | "Te-cy", 247 | "U-cy", 248 | "Ushort-cy", 249 | "Ef-cy", 250 | "Ha-cy", 251 | "Che-cy", 252 | "Tse-cy", 253 | "Sha-cy", 254 | "Shcha-cy", 255 | "Dzhe-cy", 256 | "Softsign-cy", 257 | "Yeru-cy", 258 | "Hardsign-cy", 259 | "Lje-cy", 260 | "Nje-cy", 261 | "Dze-cy", 262 | "E-cy", 263 | "Ereversed-cy", 264 | "I-cy", 265 | "Yi-cy", 266 | "Je-cy", 267 | "Tshe-cy", 268 | "Iu-cy", 269 | "Ia-cy", 270 | "Dje-cy", 271 | "Kadescender-cy", 272 | "Endescender-cy", 273 | "Ustraight-cy", 274 | "Ustraightstroke-cy", 275 | "Chedescender-cy", 276 | "Shha-cy", 277 | "Schwa-cy", 278 | "Zhedieresis-cy", 279 | "Zedieresis-cy", 280 | "Idieresis-cy", 281 | "Odieresis-cy", 282 | "Obarred-cy", 283 | "Chedieresis-cy", 284 | "Alpha", 285 | "Beta", 286 | "Gamma", 287 | "Delta", 288 | "Epsilon", 289 | "Zeta", 290 | "Eta", 291 | "Theta", 292 | "Iota", 293 | "Kappa", 294 | "Lambda", 295 | "Mu", 296 | "Nu", 297 | "Xi", 298 | "Omicron", 299 | "Pi", 300 | "Rho", 301 | "Sigma", 302 | "Tau", 303 | "Upsilon", 304 | "Phi", 305 | "Chi", 306 | "Psi", 307 | "Omega", 308 | "Alphatonos", 309 | "Epsilontonos", 310 | "Etatonos", 311 | "Iotatonos", 312 | "Omicrontonos", 313 | "Upsilontonos", 314 | "Omegatonos", 315 | "Iotadieresis", 316 | "Upsilondieresis", 317 | "KaiSymbol", 318 | ], 319 | ) 320 | 321 | 322 | def get_base_class_list(): 323 | return [ 324 | cls_zero, 325 | cls_one, 326 | cls_digit, 327 | cls_comma, 328 | cls_question, 329 | cls_uppercase, 330 | cls_normal_separator, 331 | cls_space, 332 | ] 333 | -------------------------------------------------------------------------------- /source/py/feature/base/lang.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def get_lang_list(): 5 | return [ 6 | ast.Line(""), 7 | ast.langsys("DFLT", "dflt"), 8 | ast.Line(""), 9 | ast.langsys("latn", "dflt"), 10 | ast.langsys("latn", "AZE"), 11 | ast.langsys("latn", "CRT"), 12 | ast.langsys("latn", "KAZ"), 13 | ast.langsys("latn", "TAT"), 14 | ast.langsys("latn", "TRK"), 15 | ast.langsys("latn", "ROM"), 16 | ast.langsys("latn", "MOL"), 17 | ast.langsys("latn", "CAT"), 18 | ] 19 | -------------------------------------------------------------------------------- /source/py/feature/base/locl.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | i_acc = ast.__subst("i", "idotaccent") 5 | locl_0 = ast.Lookup( 6 | "locl_latn_0", 7 | None, 8 | [ 9 | ast.script("latn"), 10 | ast.lang("AZE"), 11 | i_acc, 12 | ast.lang("CRT"), 13 | i_acc, 14 | ast.lang("KAZ"), 15 | i_acc, 16 | ast.lang("TAT"), 17 | i_acc, 18 | ast.lang("TRK"), 19 | i_acc, 20 | ], 21 | ) 22 | 23 | st_acc = ast.subst_map( 24 | ["S", "s", "T", "t"], source_suffix="cedilla", target_suffix="commaaccent" 25 | ) 26 | 27 | locl_1 = ast.Lookup( 28 | "locl_latn_1", 29 | None, 30 | [ 31 | ast.script("latn"), 32 | ast.lang("ROM"), 33 | st_acc, 34 | ast.lang("MOL"), 35 | st_acc, 36 | ], 37 | ) 38 | 39 | glyph_2 = "periodcentered" 40 | 41 | locl_2 = ast.Lookup( 42 | "locl_latn_2", 43 | None, 44 | [ 45 | ast.script("latn"), 46 | ast.lang("CAT"), 47 | ast.subst(["l"], glyph_2, ["l"], f"{glyph_2}.loclCAT"), 48 | ast.subst(["L"], glyph_2, ["L"], f"{glyph_2}.loclCAT.case"), 49 | ], 50 | ) 51 | 52 | locl_3 = ast.Lookup( 53 | "locl_latn_3", 54 | None, 55 | [ 56 | ast.script("latn"), 57 | ast.lang("NLD"), 58 | ast.__subst("ij acutecomb", "ij_acute"), 59 | ast.__subst("IJ acutecomb", "IJ_acute"), 60 | ], 61 | ) 62 | 63 | 64 | lookup_tw_name = "PunctuationTW" 65 | 66 | # Must before all features 67 | lookup_tw = ast.Lookup( 68 | lookup_tw_name, 69 | "Centered punctuations", 70 | ast.subst_map( 71 | [ 72 | "uni3001", 73 | "uni3002", 74 | "uniFF01", 75 | "uniFF0C", 76 | "uniFF1A", 77 | "uniFF1B", 78 | "uniFF1F", 79 | ], 80 | target_suffix=".tw", 81 | ), 82 | ) 83 | 84 | __locl = [ 85 | locl_0, 86 | locl_1, 87 | locl_2, 88 | locl_3, 89 | ] 90 | 91 | __locl_cn_only = [ 92 | ast.lang("ZHH"), 93 | lookup_tw.use(), 94 | ast.lang("ZHT"), 95 | lookup_tw.use(), 96 | ] 97 | 98 | def get_locl_feature_list(cn: bool, cn_only: bool = False): 99 | if not cn: 100 | return [ast.Feature("locl", __locl, "7.0")] 101 | 102 | content = __locl_cn_only if cn_only else __locl + __locl_cn_only 103 | return [lookup_tw, ast.Feature("locl", content, "7.0")] 104 | -------------------------------------------------------------------------------- /source/py/feature/base/number.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | _number_list = [ 4 | "zero", 5 | "one", 6 | "two", 7 | "three", 8 | "four", 9 | "five", 10 | "six", 11 | "seven", 12 | "eight", 13 | "nine", 14 | ] 15 | clazz_number = ast.cls(_number_list) 16 | clazz_numr = ast.cls([f"{n}.numr" for n in _number_list]) 17 | clazz_dnom = ast.cls([f"{n}.dnom" for n in _number_list]) 18 | 19 | zero = ast.subst_map( 20 | ["zero", "zero.dnom", "zero.numr", "zeroinferior", "zerosuperior"], 21 | target_suffix=".zero", 22 | ) 23 | 24 | sinf = ast.subst_map(_number_list, target_suffix="inferior") 25 | # subs is same as sinf, use another instance to correct indent 26 | subs = ast.subst_map(_number_list, target_suffix="inferior") 27 | sups = ast.subst_map(_number_list, target_suffix="superior") 28 | numr = ast.subst_map(_number_list, target_suffix=".numr") 29 | dnom = ast.subst_map(_number_list, target_suffix=".dnom") 30 | ordn = [ 31 | ast.subst(clazz_number, ast.cls("A", "a"), None, "ordfeminine"), 32 | ast.subst(clazz_number, ast.cls("O", "o"), None, "ordmasculine"), 33 | ast.__subst("N o period", "numero"), 34 | ] 35 | 36 | frac = [ 37 | ast.Lookup("FRAC", None, [ast.subst(None, "/", None, "fraction")]), 38 | ast.Lookup( 39 | "UP", 40 | None, 41 | [ast.subst(None, clazz_number, None, clazz_numr)], 42 | ), 43 | ast.Lookup( 44 | "DOWN", 45 | None, 46 | [ 47 | ast.subst("fraction", clazz_numr, None, clazz_dnom), 48 | ast.subst( 49 | clazz_dnom, 50 | clazz_numr, 51 | None, 52 | clazz_dnom, 53 | ), 54 | ], 55 | ), 56 | ] 57 | 58 | 59 | def get_number_feature_list(): 60 | return [ 61 | ast.Feature("sinf", sinf, "7.0"), 62 | ast.Feature("subs", subs, "7.0"), 63 | ast.Feature("sups", sups, "7.0"), 64 | ast.Feature("numr", numr, "7.0"), 65 | ast.Feature("dnom", dnom, "7.0"), 66 | ast.Feature("frac", frac, "7.0"), 67 | ast.Feature("ordn", ordn, "7.0"), 68 | ast.Feature("zero", zero, "7.0"), 69 | ] 70 | -------------------------------------------------------------------------------- /source/py/feature/calt/__init__.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.calt import ( 3 | asciitilde, 4 | cross, 5 | equal_arrow, 6 | escape, 7 | hyphen_arrow, 8 | italic, 9 | markup_like, 10 | pipe, 11 | tag, 12 | whitespace, 13 | ) 14 | 15 | 16 | def get_calt_lookup( 17 | cls_var: ast.Clazz, cls_hex_letter: ast.Clazz, is_italic: bool, normal: bool = False 18 | ) -> list[list[ast.Lookup]]: 19 | lookup = [ 20 | whitespace.get_lookup(cls_var), 21 | asciitilde.get_lookup(), 22 | cross.get_lookup(cls_hex_letter), 23 | markup_like.get_lookup(), 24 | equal_arrow.get_lookup(cls_var), 25 | escape.get_lookup(), 26 | hyphen_arrow.get_lookup(), 27 | pipe.get_lookup(), 28 | tag.get_lookup(cls_var), 29 | ] 30 | 31 | if is_italic and not normal: 32 | lookup += [italic.get_lookup()] 33 | 34 | return lookup 35 | 36 | 37 | def get_calt( 38 | cls_var: ast.Clazz, cls_hex_letter: ast.Clazz, is_italic: bool, is_normal: bool = False 39 | ) -> ast.Feature: 40 | return ast.Feature( 41 | "calt", get_calt_lookup(cls_var, cls_hex_letter, is_italic, is_normal), "7.0" 42 | ) 43 | -------------------------------------------------------------------------------- /source/py/feature/calt/_infinite_utils.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | __USE_INFINITE = True 5 | 6 | 7 | def use_infinite(): 8 | return __USE_INFINITE 9 | 10 | 11 | def ignore_when_using_infinite(*items: str | ast.Lookup) -> list: 12 | if __USE_INFINITE: 13 | return [] 14 | return items # type: ignore 15 | 16 | 17 | def infinite_rules( 18 | g: str, cls_start: ast.Clazz, symbols: list[str], extra_rules: list[ast.Line] = [] 19 | ): 20 | prefix = [] 21 | 22 | for s in symbols: 23 | prefix.append(ast.gly_seq(s + g, "sta")) 24 | prefix.append(ast.gly_seq(s + g, "mid")) 25 | 26 | prefix_cls = ast.cls(prefix, cls_start) 27 | 28 | return [ 29 | ast.subst(prefix_cls, g, ast.cls(symbols, g), ast.gly_seq(g, "mid")), 30 | ast.subst(prefix_cls, g, None, ast.gly_seq(g, "end")), 31 | *[ 32 | [ 33 | ast.subst(cls_start, s, g, ast.gly_seq(s + g, "mid")), 34 | ast.subst(cls_start, s, None, ast.gly_seq(s + g, "end")), 35 | ast.subst(None, s, g, ast.gly_seq(s + g, "sta")), 36 | ] 37 | for s in symbols 38 | ], 39 | *extra_rules, 40 | # Must be end of rules 41 | ast.subst(None, g, ast.cls(symbols, g), ast.gly_seq(g, "sta")), 42 | ] 43 | -------------------------------------------------------------------------------- /source/py/feature/calt/asciitilde.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.calt._infinite_utils import use_infinite 3 | 4 | 5 | def get_lookup(): 6 | start = ast.gly_seq("~", "sta") 7 | mid = ast.gly_seq("~", "mid") 8 | end = ast.gly_seq("~", "end") 9 | return [ 10 | ast.subst_liga( 11 | "<~", 12 | ign_prefix="<", 13 | ign_suffix=ast.cls("~", ">"), 14 | ), 15 | ast.subst_liga( 16 | "~>", 17 | ign_prefix=ast.cls("~", "<"), 18 | ign_suffix=">", 19 | ), 20 | ast.subst_liga( 21 | "~~", 22 | ign_prefix=ast.cls("~", "<"), 23 | ign_suffix=ast.cls("~", ">"), 24 | ), 25 | ast.subst_liga( 26 | "<~>", 27 | ign_prefix="<", 28 | ign_suffix=">", 29 | ), 30 | ast.subst_liga( 31 | "<~~", 32 | ign_prefix="<", 33 | ign_suffix=ast.cls("~", ">"), 34 | ), 35 | ast.subst_liga( 36 | "~~>", 37 | ign_prefix=ast.cls("~", "<"), 38 | ign_suffix=">", 39 | ), 40 | ast.subst_liga( 41 | "-~", 42 | ign_prefix="-", 43 | ign_suffix="~", 44 | ), 45 | ast.subst_liga( 46 | "~-", 47 | ign_prefix="~", 48 | ign_suffix="-", 49 | ), 50 | ast.subst_liga( 51 | "~@", # Cloujure 52 | ign_prefix="~", 53 | ign_suffix="@", 54 | ), 55 | ast.Lookup( 56 | "infinite_asciitilde", 57 | "~~~~~~~", 58 | [ 59 | ast.subst(ast.cls(start, mid), "~", "~", mid), 60 | ast.subst(ast.cls(start, mid), "~", None, end), 61 | ast.subst(None, "~", "~", start), 62 | ], 63 | ) 64 | if use_infinite() 65 | else None, 66 | ] 67 | -------------------------------------------------------------------------------- /source/py/feature/calt/cross.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.base.clazz import cls_zero, cls_digit 3 | 4 | 5 | def get_lookup(cls_hex_letter: ast.Clazz): 6 | return [ 7 | # Upper x for HEX numbers and width-height expression 8 | ast.Lookup( 9 | "cross", 10 | "0xA12 0x56 1920x1080", 11 | [ 12 | ast.subst(cls_zero, "x", ast.cls(cls_digit, cls_hex_letter), "multiply"), 13 | ast.subst(cls_digit, "x", cls_digit, "multiply"), 14 | ], 15 | ) 16 | ] 17 | -------------------------------------------------------------------------------- /source/py/feature/calt/equal_arrow.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.base.clazz import cls_normal_separator, cls_question 3 | from source.py.feature.calt._infinite_utils import ( 4 | use_infinite, 5 | ignore_when_using_infinite, 6 | infinite_rules, 7 | ) 8 | 9 | 10 | # Inspired by Fira Code, source: 11 | # https://github.com/tonsky/FiraCode/blob/master/features/calt/equal_arrows.fea 12 | def infinite_equals(): 13 | eq_start = ast.gly_seq("=", "sta") 14 | eq_middle = ast.gly_seq("=", "mid") 15 | eq_end = ast.gly_seq("=", "end") 16 | cls_start = ast.Clazz("EqualStart", [eq_start, eq_middle]) 17 | 18 | return ast.Lookup( 19 | "infinite_equal", 20 | " ".join( 21 | [ 22 | "<=>", 23 | "<==>", 24 | "<==", 25 | "==>", 26 | "=>", 27 | "<=|", 28 | "|=>", 29 | "=<=", 30 | "=>=", 31 | "=======", 32 | ">=<", 33 | ] 34 | ), 35 | [ 36 | cls_start.state(), 37 | ast.ign(None, "!", ["=", "="]), 38 | ast.ign("|", "|", "="), 39 | ast.ign("=", "|", "|"), 40 | ast.ign(["(", cls_question], "<", "="), 41 | ast.ign(["(", cls_question, "<"], "=", ast.cls("<", ">", "|", "=")), 42 | ast.ign(["(", cls_question, "<"], "=", ["=", ast.cls("<", ">", "|")]), 43 | # Disable >=", ["=", ast.cls(ast.SPC, ">")]), 45 | # Disable >==", ["=", "=", ast.SPC]), 47 | # Disable >===", ["=", "=", "=", ast.SPC]), 49 | ast.ign(">", "=", ["=", "=", ast.SPC]), 50 | ast.ign([">", "="], "=", ["=", ast.SPC]), 51 | *infinite_rules( 52 | g="=", 53 | cls_start=cls_start, 54 | symbols=["<", ">", "|"], 55 | extra_rules=[ 56 | ast.subst(eq_end, ":", "=", ast.gly(":", ".case", True)), 57 | # Disable >=< 58 | ast.subst( 59 | ">", "=", ["<", "="], ast.gly_seq(">=", "sta") 60 | ), 61 | # Disable =< 62 | ast.subst(None, "=", ["<", "="], eq_start), 63 | ast.ign(None, "=", "<"), 64 | ], 65 | ), 66 | ], 67 | ) 68 | 69 | 70 | def get_lookup(cls_var: ast.Clazz): 71 | return [ 72 | ignore_when_using_infinite( 73 | ast.subst_liga( 74 | "<=>", 75 | ign_prefix=ast.cls("<", "="), 76 | ign_suffix=ast.cls(">", "="), 77 | extra_rules=[ 78 | ast.ign(["(", cls_question], "<", ["=", ">"]), 79 | ], 80 | ), 81 | ast.subst_liga( 82 | "<==>", 83 | ign_prefix=ast.cls("<", "="), 84 | ign_suffix=ast.cls(">", "="), 85 | extra_rules=[ 86 | ast.ign(["(", cls_question], "<", ["=", "=", ">"]), 87 | ], 88 | ), 89 | ), 90 | ast.subst_liga( 91 | ">=", 92 | ign_prefix=ast.cls(">", "="), 93 | ign_suffix=ast.cls("<", ">", "=", "!", ast.SPC, cls_normal_separator), 94 | ), 95 | ast.subst_liga( 96 | "<=", 97 | ign_prefix=ast.cls("<", "="), 98 | ign_suffix=ast.cls("<", ">", "=", "!", ast.SPC, cls_normal_separator), 99 | extra_rules=[ 100 | ast.ign(["(", cls_question], "<", "="), 101 | ], 102 | ), 103 | ignore_when_using_infinite( 104 | ast.subst_liga( 105 | "<==", 106 | ign_prefix=ast.cls("<", "="), 107 | ign_suffix=ast.cls("=", ">", "<"), 108 | extra_rules=[ 109 | ast.ign(["(", cls_question], "<", ["=", "="]), 110 | ], 111 | ), 112 | ast.subst_liga( 113 | "==>", 114 | ign_prefix=ast.cls("[", "=", ">", "<"), 115 | ign_suffix=ast.cls(">", "="), 116 | extra_rules=[ 117 | ast.ign(["(", cls_question, "<"], "=", ["=", ">"]), 118 | ast.ign(["(", cls_question], "=", ["=", ">"]), 119 | ], 120 | ), 121 | ast.subst_liga( 122 | "=>", 123 | ign_prefix=ast.cls("[", "=", ">", "|"), 124 | ign_suffix=ast.cls("=", ">"), 125 | extra_rules=[ 126 | ast.ign(["(", cls_question, "<"], "=", ">"), 127 | ast.ign(["(", cls_question], "=", ">"), 128 | ], 129 | ), 130 | ), 131 | ast.subst_liga( 132 | "<=<", 133 | ign_prefix=ast.cls("<", "="), 134 | # `cls_var` is used to prevent confliction in Swift operator overload 135 | # 136 | # ```swift 137 | # public func <=(lhs: Expression, rhs: Expression) -> Expression where V.Datatype: Comparable 138 | # ``` 139 | ign_suffix=ast.cls("<", "=", cls_var), 140 | extra_rules=[ 141 | ast.ign(["(", cls_question], "<", ["=", "<"]), 142 | ], 143 | ), 144 | ast.subst_liga( 145 | ">=>", 146 | ign_prefix=ast.cls(">", "="), 147 | ign_suffix=ast.cls(">", "="), 148 | ), 149 | ignore_when_using_infinite( 150 | ast.subst_liga( 151 | "<=|", 152 | ign_prefix="<", 153 | ign_suffix=ast.cls("<", ">", "=", cls_normal_separator), 154 | extra_rules=[ 155 | ast.ign(["(", cls_question], "<", ["=", "|"]), 156 | ], 157 | ), 158 | ast.subst_liga( 159 | "|=>", 160 | ign_prefix=ast.cls("<", ">", "=", cls_normal_separator), 161 | ign_suffix=">", 162 | ), 163 | ), 164 | ast.subst_liga( 165 | "==", 166 | ign_prefix=ast.cls(":", "=", "!", "<", ">", "|"), 167 | ign_suffix=ast.cls(":", "=", "<", ">", "|"), 168 | extra_rules=[ 169 | ast.ign(["(", cls_question], "=", "="), 170 | ast.ign(["(", cls_question, "<"], "=", "="), 171 | ], 172 | ), 173 | ast.subst_liga( 174 | "===", 175 | ign_prefix=ast.cls("=", "<", ">", "|", ":", ast.SPC), 176 | ign_suffix=ast.cls("=", "<", ">", "|", ":", ast.SPC), 177 | extra_rules=[ 178 | ast.ign(["(", cls_question], "=", ["=", "="]), 179 | ast.ign(["(", cls_question, "<"], "=", ["=", "="]), 180 | ], 181 | ), 182 | ast.subst_liga( 183 | "===", 184 | lookup_name=ast.gly("===", "__ALT__"), 185 | desc=">===", [ast.SPC, ast.gly("", ["<", "/"]), 189 | ] 190 | ), 191 | ast.subst_liga( 192 | "!=", 193 | ign_prefix=ast.cls("!", "="), 194 | ign_suffix="=", 195 | extra_rules=[ 196 | ast.ign(["(", cls_question], "!", "="), 197 | ast.ign(["(", cls_question, "<"], "!", "="), 198 | ], 199 | ), 200 | ast.subst_liga( 201 | "!==", 202 | ign_prefix=ast.cls("!", "="), 203 | ign_suffix=ast.cls("!", "="), 204 | extra_rules=[ 205 | ast.ign(["(", cls_question], "!", ["=", "="]), 206 | ast.ign(["(", cls_question, "<"], "!", ["=", "="]), 207 | ], 208 | ), 209 | ast.subst_liga( 210 | "=/=", 211 | ign_prefix="=", 212 | ign_suffix="=", 213 | extra_rules=[ 214 | ast.ign(["(", cls_question], "=", ["/", "="]), 215 | ast.ign(["(", cls_question, "<"], "=", ["/", "="]), 216 | ], 217 | ), 218 | ast.subst_liga( 219 | "=!=", 220 | ign_prefix="=", 221 | ign_suffix="=", 222 | extra_rules=[ 223 | ast.ign(["(", cls_question], "=", ["!", "="]), 224 | ast.ign(["(", cls_question, "<"], "=", ["!", "="]), 225 | ], 226 | ), 227 | ignore_when_using_infinite( 228 | ast.subst_liga( 229 | "=<=", 230 | ign_prefix=ast.cls("=", ">", "<", "|"), 231 | ign_suffix=ast.cls("=", "<", ">"), 232 | extra_rules=[ 233 | ast.ign(["(", cls_question], "=", [">", "="]), 234 | ], 235 | ), 236 | ast.subst_liga( 237 | "=>=", 238 | ign_prefix=ast.cls("=", ">", "<", "|"), 239 | ign_suffix=ast.cls("=", "<", ">", "|"), 240 | extra_rules=[ 241 | ast.ign(["(", cls_question], "=", [">", "="]), 242 | ], 243 | ), 244 | ), 245 | ast.subst_liga( 246 | "|=", 247 | ign_prefix=ast.cls("|", "="), 248 | ign_suffix=ast.cls(">", "|", "="), 249 | ), 250 | infinite_equals() if use_infinite() else None, 251 | ] 252 | -------------------------------------------------------------------------------- /source/py/feature/calt/escape.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.base.clazz import cls_comma, cls_question 3 | 4 | 5 | def get_lookup(): 6 | escape_cls = ast.Clazz("Escape", ast.LATIN_PUNCTUATIONS + [cls_comma, cls_question]) 7 | escape_liga = ast.gly("\\", ".liga") 8 | return [ 9 | # Thin backslash (\\) to better distingish escape chars 10 | ast.Lookup( 11 | "escape", 12 | "\\\\ \\' \\.", 13 | [ 14 | ast.cls_states(escape_cls), 15 | ast.ign(escape_liga, "\\", escape_cls), 16 | ast.ign(None, "\\", ["%", "%"]), 17 | ast.subst(None, "\\", escape_cls, escape_liga), 18 | ], 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /source/py/feature/calt/hyphen_arrow.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.base.clazz import cls_digit, cls_question 3 | from source.py.feature.calt._infinite_utils import ( 4 | use_infinite, 5 | ignore_when_using_infinite, 6 | infinite_rules, 7 | ) 8 | 9 | 10 | # Inspirde by Fira Code, source: 11 | # https://github.com/tonsky/FiraCode/blob/master/features/calt/hyphen_arrows.fea 12 | def infinite_hyphens(): 13 | hy_start = ast.gly_seq("-", "sta") 14 | hy_middle = ast.gly_seq("-", "mid") 15 | cls_start = ast.Clazz("HyphenStart", [hy_start, hy_middle]) 16 | 17 | return ast.Lookup( 18 | "infinite_hyphen", 19 | " ".join( 20 | [ 21 | "<->", 22 | "<-->", 23 | "->", 24 | "<-", 25 | "-->", 26 | "<--", 27 | ">->", 28 | "<-<", 29 | "|->", 30 | "<-|", 31 | "-------", 32 | ">-", 33 | "-<", 34 | ">-<", 35 | ] 36 | ), 37 | [ 38 | cls_start.state(), 39 | ast.ign(None, "<", [ast.cls("!", "#"), "-", "-"]), 40 | ast.ign("|", "|", "-"), 41 | ast.ign("-", "|", "|"), 42 | ast.ign("-", "-", "|"), 43 | ast.ign(["(", cls_question, "<", "!"], "-", "-"), 44 | ast.ign(">", "-", "<"), 45 | ast.ign(None, "<", ["-", ast.cls("+", "/", cls_digit)]), 46 | ast.ign(None, ">", ["-", ast.SPC]), 47 | ast.ign(None, "-", ["<", "/"]), 48 | *infinite_rules("-", cls_start, ["<", ">", "|"]), 49 | ], 50 | ) 51 | 52 | 53 | def get_lookup(): 54 | return [ 55 | ast.subst_liga( 56 | "--", 57 | ign_prefix=ast.cls("<", ">", "-", "|"), 58 | ign_suffix=ast.cls("<", ">", "-", "|"), 59 | extra_rules=[ 60 | ast.ign(["<", ast.cls("#", "!")], "-", "-"), 61 | ast.ign( 62 | ["(", cls_question, "<", "!"], 63 | "-", 64 | "-", 65 | ), 66 | ], 67 | ), 68 | ast.subst_liga( 69 | "---", 70 | ign_prefix=ast.cls("<", ">", "-", "|", ast.SPC), 71 | ign_suffix=ast.cls("<", ">", "-", "|", ast.SPC), 72 | extra_rules=[ 73 | ast.ign("<", "-", ["-", "-", ">"]), 74 | ], 75 | ), 76 | ast.subst_liga( 77 | "", target="xml_empty_comment.liga"), 90 | ignore_when_using_infinite( 91 | ast.subst_liga( 92 | "<->", 93 | ign_prefix=ast.cls("<", "-"), 94 | ign_suffix=ast.cls(">", "-"), 95 | ), 96 | ast.subst_liga( 97 | "->", 98 | ign_prefix=ast.cls("-", "<", ">", "|", "+"), 99 | ign_suffix=ast.cls(">", "-"), 100 | ), 101 | ast.subst_liga( 102 | "<-", 103 | ign_prefix=ast.cls("<", "-"), 104 | ign_suffix=ast.cls("-", "<", ">", "|", "+", "/", cls_digit), 105 | ), 106 | ast.subst_liga( 107 | "-->", 108 | ign_prefix=ast.cls("-", "<", ">", "|"), 109 | ign_suffix=ast.cls(">", "-"), 110 | ), 111 | ast.subst_liga( 112 | "<--", 113 | ign_prefix=ast.cls("<", "|"), 114 | ign_suffix=ast.cls("-", "<", ">", "|"), 115 | ), 116 | ast.subst_liga( 117 | "<-<", 118 | ign_prefix=ast.cls("<", "|", "-"), 119 | ign_suffix=ast.cls("<", "|", "-"), 120 | ), 121 | ast.subst_liga( 122 | ">->", 123 | ign_prefix=ast.cls(">", "|", "-"), 124 | ign_suffix=ast.cls(">", "|", "-"), 125 | ), 126 | ast.subst_liga( 127 | "<-|", 128 | ign_prefix=ast.cls("<", "-"), 129 | ign_suffix=ast.cls("|", "-"), 130 | ), 131 | ast.subst_liga( 132 | "|->", 133 | ign_prefix=ast.cls("|", "-"), 134 | ign_suffix=ast.cls(">", "-"), 135 | ), 136 | ), 137 | infinite_hyphens() if use_infinite() else None, 138 | ] 139 | -------------------------------------------------------------------------------- /source/py/feature/calt/italic.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | 3 | 4 | def get_lookup(): 5 | return [ 6 | ast.subst_liga("Cl", ign_suffix="l"), 7 | ast.subst_liga("al", ign_suffix="l"), 8 | ast.subst_liga("cl", ign_suffix="l"), 9 | ast.subst_liga("el", ign_suffix="l"), 10 | ast.subst_liga("il", ign_suffix="l"), 11 | ast.subst_liga("tl", ign_suffix="l"), 12 | ast.subst_liga("ul", ign_suffix="l"), 13 | ast.subst_liga("xl", ign_suffix="l"), 14 | ast.subst_liga("ff", ign_prefix="f", ign_suffix="f"), 15 | ast.subst_liga("tt", ign_prefix="t", ign_suffix=ast.cls("t", "l")), 16 | ast.subst_liga("all", ign_suffix="l"), 17 | ast.subst_liga("ell", ign_suffix="l"), 18 | ast.subst_liga("ill", ign_suffix="l"), 19 | ast.subst_liga("ull", ign_suffix="l"), 20 | ast.subst_liga( 21 | "ll", 22 | ign_prefix=ast.cls( 23 | "C", 24 | "a", 25 | "c", 26 | "e", 27 | "i", 28 | "t", 29 | "u", 30 | "x", 31 | ), 32 | ign_suffix="l", 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /source/py/feature/calt/markup_like.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | 3 | 4 | def get_lookup(): 5 | return [ 6 | ast.subst_liga( 7 | "<>", 8 | ign_prefix="<", 9 | ign_suffix=">", 10 | ), 11 | ast.subst_liga( 12 | ""), 15 | ), 16 | ast.subst_liga( 17 | "/>", 18 | ign_prefix=ast.cls("<", "/"), 19 | ign_suffix=">", 20 | ), 21 | ast.subst_liga( 22 | "", 23 | ign_prefix="<", 24 | ign_suffix=">", 25 | ), 26 | ast.subst_liga( 27 | "<+", 28 | ign_prefix="<", 29 | ign_suffix=ast.cls("+", ">"), 30 | ), 31 | ast.subst_liga( 32 | "+>", 33 | ign_prefix=ast.cls("+", "<"), 34 | ign_suffix=">", 35 | ), 36 | ast.subst_liga( 37 | "<+>", 38 | ign_prefix="<", 39 | ign_suffix=">", 40 | ), 41 | ast.subst_liga( 42 | "<*", 43 | ign_prefix="<", 44 | ign_suffix=ast.cls("*", ">"), 45 | ), 46 | ast.subst_liga( 47 | "*>", 48 | ign_prefix=ast.cls("*", "<"), 49 | ign_suffix=">", 50 | ), 51 | ast.subst_liga( 52 | "<*>", 53 | ign_prefix="<", 54 | ign_suffix=">", 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /source/py/feature/calt/pipe.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.base.clazz import cls_comma 3 | 4 | def get_lookup(): 5 | return [ 6 | ast.subst_liga( 7 | "<|||", 8 | ign_prefix="<", 9 | ign_suffix=ast.cls("|", ">"), 10 | ), 11 | ast.subst_liga( 12 | "|||>", 13 | ign_prefix="|", 14 | ign_suffix=">", 15 | ), 16 | ast.subst_liga( 17 | "<||", 18 | ign_prefix="<", 19 | ign_suffix=ast.cls("|", ">"), 20 | ), 21 | ast.subst_liga( 22 | "||>", 23 | ign_prefix=ast.cls("-", "<"), 24 | ign_suffix=">", 25 | ), 26 | ast.subst_liga( 27 | "<|", 28 | ign_prefix="<", 29 | ign_suffix=ast.cls("|", ">"), 30 | ), 31 | ast.subst_liga( 32 | "|>", 33 | ign_prefix=ast.cls("-", "<", "|"), 34 | ign_suffix=ast.cls(">", "="), 35 | ), 36 | ast.subst_liga( 37 | "<|>", 38 | ign_prefix="<", 39 | ign_suffix=">", 40 | ), 41 | ast.subst_liga( 42 | "_|_", 43 | ign_prefix=ast.cls("_", "[", cls_comma), 44 | ign_suffix="_", 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /source/py/feature/calt/tag.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | 3 | built_in_tag_text = [ 4 | "trace", 5 | "debug", 6 | "info", 7 | "warn", 8 | "error", 9 | "fatal", 10 | "todo", 11 | "fixme", 12 | "note", 13 | "hack", 14 | "mark", 15 | "eror", 16 | "warning", 17 | ] 18 | 19 | def tag_upper(text_list: list[str]): 20 | """ 21 | Create ligature substitution rules for uppercase tag sequences. 22 | 23 | This function takes a list of text strings and generates ligature substitution rules 24 | for each text, where the text is converted to uppercase and wrapped in square brackets. 25 | 26 | Args: 27 | text_list (list[str]): A list of strings to be converted into tag ligature rules. 28 | 29 | Returns: 30 | list: A list of substitution rules where each rule replaces a sequence of 31 | uppercase characters enclosed in brackets with a corresponding ligature. 32 | 33 | Example: 34 | >>> tag_upper(['todo']) 35 | # Generates rule to substitute '[TODO]' with 'tag_todo.liga' 36 | """ 37 | result = [] 38 | 39 | for text in text_list: 40 | if text not in built_in_tag_text: 41 | print(f"{text} is not in {built_in_tag_text}, skip") 42 | continue 43 | 44 | source = ["["] + [g.upper() for g in text] + ["]"] 45 | result.append( 46 | ast.subst_liga( 47 | source, 48 | target=f"tag_{text}.liga", 49 | lookup_name=f"tag_{text}", 50 | desc="".join(source), 51 | ) 52 | ) 53 | 54 | return result 55 | 56 | 57 | def tag_any(text_list: list[str], cls_var: ast.Clazz): 58 | """ 59 | Generate substitution rules for tags based on a list of text strings. 60 | 61 | This function creates ligature substitution rules for tag-like structures, 62 | where each text string is converted into a tag format with parentheses. 63 | 64 | Args: 65 | text_list (list[str]): A list of strings to be converted into tag formats 66 | cls_var (ast.Clazz): A class variable used for ignoring specific glyph combinations 67 | 68 | Returns: 69 | list: A list of substitution rules (ast.subst_liga objects) for tag formations 70 | 71 | Example: 72 | >>> tag_any(['todo', 'fixme'], my_class) 73 | # Creates substitution rules for 'todo))' -> 'tag_hello.liga' 74 | # and 'fixme))' -> 'tag_fixme.liga' 75 | """ 76 | result = [] 77 | 78 | for text in text_list: 79 | if text not in built_in_tag_text: 80 | print(f"{text} is not in {built_in_tag_text}, skip") 81 | continue 82 | 83 | glyphs = [f"@{g.upper()}" for g in text] + [")", ")"] 84 | result.append( 85 | ast.subst_liga( 86 | glyphs, 87 | target=f"tag_{text}.liga", 88 | lookup_name=f"tag_{text}_alt", 89 | desc=f"{text}))", 90 | ign_prefix=cls_var, 91 | ) 92 | ) 93 | 94 | return result 95 | 96 | 97 | __map = { 98 | "<": "sharp_start", 99 | ">": "sharp_end", 100 | "(": "circle_start", 101 | ")": "circle_end", 102 | "[": "block_start", 103 | "]": "block_end", 104 | } 105 | 106 | 107 | def tag_custom( 108 | content_list: list[tuple[str, str]], 109 | bg_cls_dict: dict[str, ast.Clazz], 110 | ): 111 | """ 112 | Generate custom tag lookup. 113 | Args: 114 | content_list (list[tuple[str, str]]): A list of tuples containing: 115 | - source: Original string sequence to match 116 | - target: Target pattern to replace with. Must follow these rules: 117 | - End with one of ["<", ">", "(", ")", "[", "]"] 118 | - Middle characters must be ASCII letters 119 | 120 | bg_cls_dict (dict[str, ast.Clazz]): Dictionary mapping uppercase letters to background 121 | class definitions 122 | Returns: 123 | ast.Lookup: A Lookup object containing the substitution rules, named with pattern 124 | "custom_tag_{target middle chars}". 125 | Example: 126 | >>> tag_custom("_TODO_", "(TODO)") 127 | """ 128 | result = [] 129 | for source, target in content_list: 130 | glyphs = list(source) 131 | glyphs_len = len(glyphs) 132 | target_len = len(target) 133 | 134 | if target_len != glyphs_len: 135 | raise ValueError( 136 | f"length of `content` ({glyphs_len}) must be equal to length of `target` ({target_len})." 137 | ) 138 | if target[-1] not in __map: 139 | raise ValueError( 140 | f"Last letter of `target` must in {list(__map.keys())}, current is '{target[-1]}'" 141 | ) 142 | 143 | # Parse source 144 | source_list = [] 145 | for g in glyphs: 146 | if g.isalpha(): 147 | source_list.append(f"@{g.upper()}") 148 | else: 149 | source_list.append(ast.gly(g)) 150 | 151 | # Parse target 152 | target_list = [] 153 | for target_gly in target: 154 | if target_gly in __map: 155 | target_list.append(f"{__map[target_gly]}.bg") 156 | elif target_gly.isalpha(): 157 | up = target_gly.upper() 158 | if up in bg_cls_dict: 159 | target_list.append(bg_cls_dict[up]) 160 | else: 161 | target_list.append(f"{up}.bg") 162 | else: 163 | raise Exception( 164 | f"All tag content must be in ASCII letters or {list(__map.keys())}, current is {target[1:-1]}" 165 | ) 166 | 167 | # Generate substitutions in reverse order (from last glyph to first) 168 | subst_list = [] 169 | for i in range(glyphs_len, 0, -1): 170 | before = target_list[: i - 1] 171 | glyph = source_list[i - 1] 172 | after = source_list[i:] if i < glyphs_len else None 173 | replace = target_list[i - 1] 174 | if isinstance(replace, ast.Clazz): 175 | replace = replace.glyphs[0] 176 | subst_list.append(ast.subst(before, glyph, after, replace)) 177 | 178 | desc = [] 179 | for item in source_list: 180 | if isinstance(item, str): 181 | desc.append(item.replace("@", "")) 182 | elif isinstance(item, ast.Clazz): 183 | desc.append(f"_{item.name}_") 184 | 185 | lookup_name = f"custom_tag_{'_'.join(desc)}" 186 | 187 | result.append( 188 | ast.Lookup( 189 | name=lookup_name, 190 | desc=source, 191 | content=subst_list, 192 | ) 193 | ) 194 | 195 | return result 196 | 197 | 198 | 199 | 200 | def get_lookup(cls_var: ast.Clazz): 201 | # Dict to map letter and class. 202 | # Only letter that has uppercase variant will be added. 203 | # {"q": ast.Clazz("BgQ", ["Q", "Q.cv01"])} 204 | bg_cls_dict = {} 205 | for item in cls_var.glyphs: 206 | if not isinstance(item, ast.Clazz): 207 | continue 208 | 209 | first = item.glyphs[0] 210 | if not isinstance(first, str) or len(first) > 1 or not first.isalpha(): 211 | continue 212 | 213 | gly_list = [f"{first}.bg"] 214 | for gly in item.glyphs[1:]: 215 | if isinstance(gly, str) and gly.startswith(first): 216 | _, feat = gly.split(".", 1) 217 | gly_list.append(f"{first}.bg.{feat}") 218 | 219 | if len(gly_list) > 1: 220 | bg_cls_dict[first] = ast.Clazz(f"Bg{first.capitalize()}", gly_list) 221 | 222 | return [ 223 | ast.cls_states(*bg_cls_dict.values()), 224 | tag_upper(built_in_tag_text), 225 | tag_any(["todo", "fixme"], cls_var), 226 | # ========================================================= 227 | # Custom tags 228 | # --------------------------------------------------------- 229 | tag_custom( 230 | [ 231 | # ("_bug_", "[bug]"), 232 | # ("_noqa_", "(noqa)"), 233 | ], 234 | bg_cls_dict, 235 | ), 236 | # ========================================================= 237 | # Mark annotation in Xcode 238 | # example: `// TODO: code review` 239 | # --------------------------------------------------------- 240 | # ast.subst_liga( 241 | # source="TODO:", 242 | # target="tag_todo.liga", 243 | # lookup_name="todo_colon" 244 | # ) 245 | # ast.subst_liga( 246 | # source="MARK:", 247 | # target="tag_todo.liga", 248 | # lookup_name="mark_colon" 249 | # ) 250 | # ========================================================= 251 | ] 252 | -------------------------------------------------------------------------------- /source/py/feature/calt/whitespace/__init__.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.base.clazz import cls_question 3 | from source.py.feature.calt.whitespace import ( 4 | brace, 5 | colon, 6 | multiple_compare, 7 | numbersign_underscore, 8 | upper, 9 | ) 10 | 11 | 12 | def get_base_lookup(): 13 | return [ 14 | ast.subst_liga( 15 | "[|", 16 | ign_prefix="[", 17 | ign_suffix=ast.cls("]", "|"), 18 | ), 19 | ast.subst_liga( 20 | "|]", 21 | ign_prefix=ast.cls("[", "|"), 22 | ign_suffix="]", 23 | ), 24 | ast.subst_liga( 25 | "!!", 26 | ign_prefix="!", 27 | ign_suffix="!", 28 | extra_rules=[ 29 | ast.ign(["(", cls_question], "!", "!"), 30 | ast.ign(["(", cls_question, "<"], "!", "!"), 31 | ], 32 | ), 33 | ast.subst_liga( 34 | "||", 35 | ign_prefix=ast.cls("|", "[", "<"), 36 | ign_suffix=ast.cls("|", "]", ">"), 37 | ), 38 | ast.subst_liga( 39 | 2 * [cls_question.use()], 40 | target=ast.gly("??"), 41 | desc="??", 42 | ign_prefix=cls_question, 43 | ign_suffix=cls_question, 44 | ), 45 | ast.subst_liga( 46 | 3 * [cls_question.use()], 47 | target=ast.gly("???"), 48 | desc="???", 49 | ign_prefix=cls_question, 50 | ign_suffix=cls_question, 51 | ), 52 | ast.subst_liga( 53 | "&&", 54 | ign_prefix="&", 55 | ign_suffix="&", 56 | ), 57 | ast.subst_liga( 58 | "&&&", 59 | ign_prefix="&", 60 | ign_suffix="&", 61 | ), 62 | ast.subst_liga( 63 | "//", 64 | ign_prefix="/", 65 | ign_suffix="/", 66 | ), 67 | ast.subst_liga( 68 | "///", 69 | ign_prefix="/", 70 | ign_suffix="/", 71 | ), 72 | ast.subst_liga( 73 | "/*", 74 | ign_prefix=ast.cls("/", "*"), 75 | ign_suffix=ast.cls("/", "*", "."), 76 | ), 77 | ast.subst_liga( 78 | "/**", 79 | ign_prefix=ast.cls("/", "*"), 80 | ign_suffix=ast.cls("/", "*", "."), 81 | ), 82 | ast.subst_liga( 83 | "*/", 84 | ign_prefix=ast.cls("/", "*", "."), 85 | ign_suffix=ast.cls("/", "*"), 86 | ), 87 | ast.subst_liga( 88 | "++", 89 | ign_prefix=ast.cls("+", ":"), 90 | ign_suffix=ast.cls("+", ":"), 91 | ), 92 | ast.subst_liga( 93 | "+++", 94 | ign_prefix="+", 95 | ign_suffix="+", 96 | ), 97 | ast.subst_liga( 98 | ";;", 99 | ign_prefix=";", 100 | ign_suffix=";", 101 | ), 102 | ast.subst_liga( 103 | ";;;", 104 | ign_prefix=";", 105 | ign_suffix=";", 106 | ), 107 | ast.subst_liga( 108 | "..", 109 | ign_prefix=".", 110 | ign_suffix=ast.cls(".", "<", cls_question), 111 | ), 112 | ast.subst_liga( 113 | "...", 114 | ign_prefix=".", 115 | ign_suffix=ast.cls(".", "<", cls_question), 116 | ), 117 | ast.subst_liga( 118 | [ast.gly("."), cls_question.use()], # Zig 119 | target=ast.gly(".?"), 120 | desc=".?", 121 | ign_prefix=".", 122 | ign_suffix=cls_question, 123 | ), 124 | ast.subst_liga( 125 | [cls_question.use(), ast.gly(".")], # TypeScript / Rust 126 | target=ast.gly("?."), 127 | desc="?.", 128 | ign_prefix=cls_question, 129 | ign_suffix=ast.cls(".", "=", cls_question), 130 | ), 131 | ast.subst_liga( 132 | "..<", # Swift / Kotlin 133 | ign_prefix=".", 134 | ign_suffix=ast.cls("<", "/", ">"), 135 | ), 136 | ast.subst_liga( 137 | ".=", # Swift 138 | ign_prefix=".", 139 | ign_suffix=ast.cls("=", ">"), 140 | ), 141 | ] 142 | 143 | 144 | def get_lookup(cls_var: ast.Clazz): 145 | return ( 146 | upper.get_lookup() 147 | + colon.get_lookup() 148 | + numbersign_underscore.get_lookup() 149 | + multiple_compare.get_lookup(cls_var) 150 | + brace.get_lookup() 151 | + get_base_lookup() 152 | ) 153 | -------------------------------------------------------------------------------- /source/py/feature/calt/whitespace/brace.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | 3 | 4 | def get_lookup(): 5 | left_start = ast.gly_seq("{", "sta") 6 | left_end = ast.gly_seq("{", "end") 7 | right_start = ast.gly_seq("}", "sta") 8 | right_end = ast.gly_seq("}", "end") 9 | return [ 10 | ast.Lookup( 11 | ast.gly("{{"), 12 | "{{", 13 | [ 14 | ast.ign("{", "{", "{"), 15 | ast.ign(None, "{", ["{", ast.cls("{", "!")]), 16 | ast.ign(None, "{", ["{", "-", "-"]), 17 | ast.subst(None, "{", "{", left_start), 18 | ast.subst(left_start, "{", None, left_end), 19 | ], 20 | ), 21 | ast.Lookup( 22 | ast.gly("}}"), 23 | "}}", 24 | [ 25 | ast.ign(ast.cls("!", "}", "-"), "}", "}"), 26 | ast.ign(None, "}", ["}", "}"]), 27 | ast.subst(None, "}", "}", right_start), 28 | ast.subst(right_start, "}", None, right_end), 29 | ], 30 | ), 31 | ast.subst_liga( 32 | "{|", 33 | ign_prefix="{", 34 | ign_suffix=ast.cls("|", "}"), 35 | ), 36 | ast.subst_liga( 37 | "|}", 38 | ign_prefix=ast.cls("{", "|"), 39 | ign_suffix="}", 40 | ), 41 | ast.subst_liga( 42 | "{{--", 43 | ign_prefix="{", 44 | ign_suffix="-", 45 | ), 46 | ast.subst_liga( 47 | "{{!--", 48 | ign_prefix="{", 49 | ign_suffix="-", 50 | ), 51 | ast.subst_liga( 52 | "--}}", 53 | ign_prefix="-", 54 | ign_suffix="}", 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /source/py/feature/calt/whitespace/colon.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.base.clazz import cls_question 3 | 4 | 5 | def get_lookup(): 6 | cls_ign_colon = ast.Clazz("IgnoreColon", ["<", ":", ">", "="]) 7 | cls_ign_markup = ast.Clazz("IgnoreMarkup", ["<", "/", ">"]) 8 | 9 | return [ 10 | ast.subst_liga( 11 | "::", 12 | ign_prefix=":", 13 | ign_suffix=ast.cls("=", ":"), 14 | ), 15 | ast.subst_liga( 16 | ":::", 17 | ign_prefix=":", 18 | ign_suffix=":", 19 | ), 20 | ast.subst_liga( 21 | [cls_question.use(), ast.gly(":")], 22 | target=ast.gly("?:"), 23 | desc="?:", 24 | ign_prefix=cls_question, 25 | ign_suffix=ast.cls(":", "="), 26 | ), 27 | ast.subst_liga( 28 | [ast.gly(":"), cls_question.use()], 29 | target=ast.gly(":?"), 30 | desc=":?", 31 | ign_prefix=":", 32 | ign_suffix=ast.cls(cls_question, ">"), 33 | ), 34 | ast.subst_liga( 35 | [ast.gly(":"), cls_question.use(), ast.gly(">")], 36 | target=ast.gly(":?>"), 37 | desc=":?>", 38 | ign_prefix=":", 39 | ign_suffix=">", 40 | ), 41 | ast.cls_states( 42 | cls_ign_colon, 43 | cls_ign_markup, 44 | ), 45 | ast.subst_liga( 46 | ":=", 47 | ign_prefix=ast.cls(cls_ign_colon, cls_question), 48 | ign_suffix=ast.cls("=", ":"), 49 | ), 50 | ast.subst_liga( 51 | "=:", 52 | ign_prefix=cls_ign_colon, 53 | ign_suffix=ast.cls("=", ":"), 54 | extra_rules=[ 55 | ast.ign(["(", cls_question], "=", ":"), 56 | ], 57 | ), 58 | ast.subst_liga( 59 | ":=:", 60 | ign_prefix=ast.cls(cls_ign_colon, cls_question), 61 | ign_suffix=ast.cls(cls_ign_colon, cls_question), 62 | extra_rules=[ 63 | ast.ign(["(", cls_question], ":", ["=", ":"]), 64 | ], 65 | ), 66 | ast.subst_liga( 67 | "=:=", 68 | ign_prefix="=", 69 | ign_suffix="=", 70 | extra_rules=[ 71 | ast.ign(["(", cls_question], "=", [":", "="]), 72 | ], 73 | ), 74 | ast.subst_liga( 75 | "<:", 76 | ign_prefix="<", 77 | ign_suffix=cls_ign_colon, 78 | ), 79 | ast.subst_liga( 80 | ":>", 81 | ign_prefix=cls_ign_colon, 82 | ign_suffix=">", 83 | ), 84 | ast.subst_liga( 85 | ":<", 86 | ign_prefix=cls_ign_colon, 87 | ign_suffix=cls_ign_markup, 88 | ), 89 | ast.subst_liga( 90 | "<:<", # scala / haskell 91 | ign_prefix="<", 92 | ign_suffix=cls_ign_markup, 93 | ), 94 | ast.subst_liga( 95 | ">:>", # scala / haskell 96 | ign_prefix=cls_ign_markup, 97 | ign_suffix=">", 98 | ), 99 | ast.subst_liga( 100 | "::=", 101 | ign_prefix=":", 102 | ign_suffix="=", 103 | ), 104 | ] 105 | -------------------------------------------------------------------------------- /source/py/feature/calt/whitespace/multiple_compare.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.base.clazz import cls_digit, cls_space 3 | 4 | 5 | def get_lookup(cls_var: ast.Clazz): 6 | cls_leading_symbol_liga = ast.Clazz("LeadingSymbolLiga", ["++", "--", "__"]) 7 | cls_equal_hyphen = ast.Clazz("EqualHyphen", ["=", "-"]) 8 | cls_symbol_before_greater = ast.Clazz( 9 | "SymbolBeforeGreater", 10 | ["|", "!", "~", "~", "#", "%", cls_space, cls_equal_hyphen], 11 | ) 12 | cls_number = ast.Clazz("Number", ["+", "-", cls_digit]) 13 | cls_quote_like = ast.Clazz("QuoteLike", ["`", "'", '"']) 14 | 15 | surround = [ 16 | (cls_var, [cls_space, ast.SPC, cls_leading_symbol_liga]), 17 | (cls_var, [ast.SPC, cls_leading_symbol_liga]), 18 | (cls_var, ast.cls(cls_var, cls_number)), 19 | (cls_symbol_before_greater, None), 20 | (None, [cls_space, cls_number]), 21 | (None, ast.cls("/", cls_number, cls_equal_hyphen)), 22 | (cls_quote_like, cls_quote_like), 23 | ] 24 | 25 | return [ 26 | ast.subst_liga( 27 | "<<", 28 | ign_prefix="<", 29 | ign_suffix=ast.cls("<", "~"), 30 | ), 31 | ast.subst_liga( 32 | "<<<", 33 | ign_prefix="<", 34 | ign_suffix="<", 35 | ), 36 | ast.cls_states( 37 | cls_leading_symbol_liga, 38 | cls_equal_hyphen, 39 | cls_symbol_before_greater, 40 | cls_number, 41 | cls_quote_like, 42 | ), 43 | ast.subst_liga( 44 | ">>", 45 | ign_prefix=ast.cls("<", "/", ">"), 46 | ign_suffix=">", 47 | surround=surround, 48 | ), 49 | ast.subst_liga( 50 | ">>>", 51 | ign_prefix=">", 52 | ign_suffix=">", 53 | surround=surround, 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /source/py/feature/calt/whitespace/numbersign_underscore.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.base.clazz import cls_question 3 | from source.py.feature.calt._infinite_utils import use_infinite 4 | 5 | 6 | def get_lookup(): 7 | start = ast.gly_seq("#", "sta") 8 | mid = ast.gly_seq("#", "mid") 9 | end = ast.gly_seq("#", "end") 10 | return [ 11 | ast.subst_liga( 12 | "__", 13 | ign_prefix=ast.cls("_", "#"), 14 | ign_suffix="_", 15 | ), 16 | ast.subst_liga( 17 | "#{", 18 | ign_prefix="#", 19 | ign_suffix="{", 20 | ), 21 | ast.subst_liga( 22 | "#[", 23 | ign_prefix="#", 24 | ign_suffix="[", 25 | ), 26 | ast.subst_liga( 27 | "#(", 28 | ign_prefix="#", 29 | ign_suffix="(", 30 | ), 31 | ast.subst_liga( 32 | [ast.gly("#"), cls_question.use()], 33 | target=ast.gly("#?"), 34 | desc="#?", 35 | ign_prefix="#", 36 | ign_suffix=cls_question, 37 | ), 38 | ast.subst_liga( 39 | "#!", 40 | ign_prefix="#", 41 | ign_suffix=ast.cls("!", "="), 42 | ), 43 | ast.subst_liga( 44 | "#:", 45 | ign_prefix="#", 46 | ign_suffix=ast.cls(":", "="), 47 | ), 48 | ast.subst_liga( 49 | "#=", 50 | ign_prefix="#", 51 | ign_suffix="=", 52 | ), 53 | ast.subst_liga( 54 | "#_", 55 | ign_suffix=ast.cls("_", "("), 56 | ), 57 | ast.subst_liga( 58 | "#__", 59 | ign_suffix="_", 60 | ), 61 | ast.subst_liga( 62 | "#_(", 63 | ign_suffix="(", 64 | ), 65 | ast.subst_liga( 66 | "]#", 67 | ign_prefix="]", 68 | ign_suffix="#", 69 | ), 70 | ast.Lookup( 71 | "infinite_numbersigns", 72 | "#######", 73 | [ 74 | ast.subst(ast.cls(start, mid), "#", "#", mid), 75 | ast.subst(ast.cls(start, mid), "#", None, end), 76 | ast.subst(None, "#", "#", start), 77 | ], 78 | ) 79 | if use_infinite() 80 | else None, 81 | ] 82 | -------------------------------------------------------------------------------- /source/py/feature/calt/whitespace/upper.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.base.clazz import cls_digit, cls_uppercase 3 | 4 | 5 | def get_lookup(): 6 | dbls = "germandbls" 7 | dbls_calt = f"{dbls}.calt" 8 | return [ 9 | ast.Lookup( 10 | "uppercase_colon", 11 | None, 12 | [ 13 | ast.subst( 14 | ast.cls(cls_digit, cls_uppercase), 15 | ":", 16 | ast.cls(cls_digit, cls_uppercase), 17 | ast.gly(":", ".case"), 18 | ) 19 | ], 20 | ), 21 | ast.Lookup( 22 | "uppercase_sharp_s", 23 | None, 24 | [ 25 | ast.subst([cls_uppercase, cls_uppercase], dbls, None, dbls_calt), 26 | ast.subst(None, dbls, cls_uppercase, dbls_calt), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /source/py/feature/cv/_common.py: -------------------------------------------------------------------------------- 1 | GLYPHS_A = [ 2 | "a", 3 | "aacute", 4 | "abreve", 5 | "abreveacute", 6 | "abrevedotbelow", 7 | "abrevegrave", 8 | "abrevehookabove", 9 | "abrevetilde", 10 | "acaron", 11 | "acircumflex", 12 | "acircumflexacute", 13 | "acircumflexdotbelow", 14 | "acircumflexgrave", 15 | "acircumflexhookabove", 16 | "acircumflextilde", 17 | "adieresis", 18 | "adotbelow", 19 | "agrave", 20 | "ahookabove", 21 | "amacron", 22 | "aogonek", 23 | "aring", 24 | "atilde", 25 | "a-cy", 26 | "ordfeminine", 27 | ] 28 | 29 | GLYPHS_I = [ 30 | "i", 31 | "iacute", 32 | "ibreve", 33 | "icaron", 34 | "icircumflex", 35 | "idieresis", 36 | "idotaccent", 37 | "idotbelow", 38 | "idotless", 39 | "igrave", 40 | "ihookabove", 41 | "imacron", 42 | "iogonek", 43 | "itilde", 44 | "idotbelowdotless", 45 | "iogonekdotless", 46 | ] 47 | 48 | GLYPHS_L = [ 49 | "l", 50 | "lacute", 51 | "lcaron", 52 | "lcommaaccent", 53 | "ldot", 54 | "lslash", 55 | ] 56 | 57 | GLYPHS_ONE = [ 58 | "one", 59 | "one.dnom", 60 | "one.numr", 61 | "oneinferior", 62 | "onesuperior", 63 | ] 64 | 65 | GLYPHS_G = [ 66 | "g", 67 | "gacute", 68 | "gbreve", 69 | "gcaron", 70 | "gcircumflex", 71 | "gcommaaccent", 72 | "gdotaccent", 73 | ] 74 | 75 | GLYPHS_J_UPPER = [ 76 | "J", 77 | "Jcircumflex", 78 | "J.bg" 79 | ] 80 | 81 | GLYPHS_R = [ 82 | "r", 83 | "racute", 84 | "rcaron", 85 | "rcommaaccent", 86 | ] 87 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv01.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.calt._infinite_utils import ignore_when_using_infinite 3 | 4 | 5 | sfx = ".cv01" 6 | 7 | 8 | def cv01_subst(): 9 | return [ 10 | ast.subst_map( 11 | "$", 12 | target_suffix=sfx, 13 | ), 14 | ast.subst_map( 15 | "%", 16 | target_suffix=sfx, 17 | ), 18 | ast.subst_map( 19 | ["&", "&&", "&&&"], 20 | target_suffix=sfx, 21 | ), 22 | ast.subst_map( 23 | ["@", "~@"], 24 | target_suffix=sfx, 25 | ), 26 | ast.subst_map( 27 | ["Q", "Q.bg"], 28 | target_suffix=sfx, 29 | ), 30 | ast.subst_map( 31 | [ 32 | "<=<", 33 | ">=>", 34 | " 37 | *ignore_when_using_infinite( 38 | "=>", 39 | "<==", 40 | "==>", 41 | "<=>", 42 | "<==>", 43 | "<=|", 44 | "|=>", 45 | "<-|", 46 | "|->", 47 | "<-", 48 | "->", 49 | "<--", 50 | "-->", 51 | "<-<", 52 | ">->", 53 | "<->", 54 | ), 55 | ast.gly_seq("<=", "sta"), 56 | ast.gly_seq(">=", "end"), 57 | ast.gly_seq("<-", "sta"), 58 | ast.gly_seq(">-", "end"), 59 | ], 60 | target_suffix=sfx, 61 | ), 62 | ] 63 | 64 | 65 | cv01_desc = "Normalize special symbols (`@ $ & % Q => ->`)" 66 | cv01_feat_regular = cv01_feat_italic = ast.CharacterVariant( 67 | id=1, desc=cv01_desc, content=cv01_subst(), version="7.0", example="@$&" 68 | ) 69 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv02.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_A 3 | 4 | 5 | def cv02_subst(): 6 | return ast.subst_map( 7 | GLYPHS_A, 8 | target_suffix=".cv02", 9 | ) 10 | 11 | 12 | cv02_name = "Alternative `a` with top arm, no effect in italic style" 13 | cv02_feat_regular = ast.CharacterVariant( 14 | id=2, desc=cv02_name, content=cv02_subst(), version="7.0", example="a" 15 | ) 16 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv03.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_I 3 | 4 | 5 | def cv03_subst(): 6 | return ast.subst_map( 7 | GLYPHS_I, 8 | target_suffix=".cv03", 9 | ) 10 | 11 | 12 | cv03_name = "Alternative `i` without left bottom bar" 13 | cv03_feat_regular = ast.CharacterVariant( 14 | id=3, desc=cv03_name, content=cv03_subst(), version="7.0", example="i" 15 | ) 16 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv04.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.cv._common import GLYPHS_L, GLYPHS_ONE 3 | 4 | 5 | def cv04_subst_regular(): 6 | return ast.subst_map( 7 | GLYPHS_L + GLYPHS_ONE, 8 | target_suffix=".cv04", 9 | ) 10 | 11 | 12 | def cv04_subst_italic(): 13 | return ast.subst_map( 14 | [ 15 | *GLYPHS_L, 16 | *GLYPHS_ONE, 17 | ast.gly("Cl"), 18 | ast.gly("al"), 19 | ast.gly("cl"), 20 | ast.gly("el"), 21 | ast.gly("il"), 22 | ast.gly("ll"), 23 | ast.gly("tl"), 24 | ast.gly("ul"), 25 | ast.gly("xl"), 26 | ast.gly("all"), 27 | ast.gly("ell"), 28 | ast.gly("ill"), 29 | ast.gly("ull"), 30 | ], 31 | target_suffix=".cv04", 32 | ) 33 | 34 | 35 | cv04_name = "Alternative `l` with left bottom bar, like consolas, will be overrided by `cv35` in italic style" 36 | cv04_feat_regular = ast.CharacterVariant( 37 | id=4, desc=cv04_name, content=cv04_subst_regular(), version="7.0", example="l1" 38 | ) 39 | cv04_feat_italic = ast.CharacterVariant( 40 | id=4, desc=cv04_name, content=cv04_subst_italic(), version="7.0", example="l1" 41 | ) 42 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv05.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_G 3 | 4 | 5 | # https://github.com/subframe7536/maple-font/issues/329 6 | def cv05_subst(): 7 | return ast.subst_map( 8 | GLYPHS_G, 9 | target_suffix=".cv05", 10 | ) 11 | 12 | 13 | cv05_name = "Alternative `g` in double story style, no effect in italic style" 14 | cv05_feat_regular = ast.CharacterVariant( 15 | id=5, desc=cv05_name, content=cv05_subst(), version="7.1", example="g" 16 | ) 17 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv06.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_I 3 | 4 | 5 | # https://github.com/subframe7536/maple-font/issues/324 6 | def cv06_subst(): 7 | return [ 8 | ast.subst_map( 9 | GLYPHS_I, 10 | target_suffix=".cv06", 11 | ), 12 | ast.subst_map( 13 | GLYPHS_I, 14 | source_suffix=".cv03", 15 | target_suffix=".cv06", 16 | ), 17 | ] 18 | 19 | 20 | cv06_name = "Alternative `i` without bottom bar, no effect in italic style" 21 | cv06_feat_regular = ast.CharacterVariant( 22 | id=6, desc=cv06_name, content=cv06_subst(), version="7.1", example="i" 23 | ) 24 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv07.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_J_UPPER 3 | 4 | 5 | # https://github.com/subframe7536/maple-font/issues/324 6 | def cv07_subst(): 7 | return [ 8 | ast.subst_map( 9 | GLYPHS_J_UPPER, 10 | target_suffix=".cv07", 11 | ), 12 | ] 13 | 14 | 15 | cv07_name = "Alternative `J` without top bar, no effect in italic style" 16 | cv07_feat_regular = ast.CharacterVariant( 17 | id=7, desc=cv07_name, content=cv07_subst(), version="7.1", example="J" 18 | ) 19 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv08.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_R 3 | 4 | 5 | # https://github.com/subframe7536/maple-font/issues/328 6 | def cv08_subst(): 7 | return [ 8 | ast.subst_map( 9 | GLYPHS_R, 10 | target_suffix=".cv08", 11 | ), 12 | ] 13 | 14 | 15 | cv08_name = "Alternative `r` with bottom bar, no effect in italic style" 16 | cv08_feat_regular = ast.CharacterVariant( 17 | id=8, desc=cv08_name, content=cv08_subst(), version="7.1", example="r" 18 | ) 19 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv31.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_A 3 | 4 | 5 | def cv31_subst(): 6 | return ast.subst_map( 7 | [ 8 | *GLYPHS_A, 9 | # Ligature variants 10 | ast.gly("al"), 11 | ast.gly("all"), 12 | ast.gly("al", ".cv04"), 13 | ast.gly("all", ".cv04"), 14 | ], 15 | target_suffix=".cv31", 16 | ) 17 | 18 | 19 | cv31_name = "Alternative italic `a` with top arm" 20 | cv31_feat_italic = ast.CharacterVariant( 21 | id=31, desc=cv31_name, content=cv31_subst(), version="7.0", example="a" 22 | ) 23 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv32.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def cv32_subst(): 5 | return ast.subst_map(["f", ast.gly("ff")], target_suffix=".cv32") 6 | 7 | 8 | cv32_name = "Alternative Italic `f` without bottom tail" 9 | cv32_feat_italic = ast.CharacterVariant( 10 | id=32, desc=cv32_name, content=cv32_subst(), version="7.0", example="f" 11 | ) 12 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv33.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_I 3 | 4 | 5 | def cv33_subst(): 6 | def gen(*suffix_list: str): 7 | result: list[str] = [] 8 | 9 | for suf in suffix_list: 10 | result.append(ast.gly("il", suf)) 11 | result.append(ast.gly("ill", suf)) 12 | 13 | return result 14 | return ast.subst_map( 15 | [ 16 | *GLYPHS_I, 17 | "j", 18 | "jcircumflex", 19 | "jdotless", 20 | *gen("", ".cv04"), 21 | ], 22 | target_suffix=".cv33", 23 | ) 24 | 25 | 26 | cv33_name = "Alternative Italic `i` and `j` with left bottom bar and horizen top bar" 27 | cv33_feat_italic = ast.CharacterVariant( 28 | id=33, desc=cv33_name, content=cv33_subst(), version="7.0", example="i" 29 | ) 30 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv34.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def cv34_subst(): 5 | return ast.subst_map(["k", "kcommaaccent"], target_suffix=".cv34") 6 | 7 | 8 | cv34_name = "Alternative Italic `k` without center circle" 9 | cv34_feat_italic = ast.CharacterVariant( 10 | id=34, desc=cv34_name, content=cv34_subst(), version="7.0", example="k" 11 | ) 12 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv35.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_L, GLYPHS_ONE 3 | 4 | 5 | def cv35_subst(): 6 | base_glyphs = [ 7 | *GLYPHS_L, 8 | ast.gly("Cl"), 9 | ast.gly("al"), 10 | ast.gly("cl"), 11 | ast.gly("el"), 12 | ast.gly("il"), 13 | ast.gly("ll"), 14 | ast.gly("tl"), 15 | ast.gly("ul"), 16 | ast.gly("xl"), 17 | ast.gly("all"), 18 | ast.gly("ell"), 19 | ast.gly("ill"), 20 | ast.gly("ull"), 21 | ] 22 | 23 | # previous cv 24 | overwrite_glyphs = { 25 | ast.gly("al"): ".cv31", 26 | ast.gly("all"): ".cv31", 27 | ast.gly("il"): ".cv33", 28 | ast.gly("ill"): ".cv33", 29 | } 30 | 31 | suf_cv04 = ".cv04" 32 | suf_cv35 = ".cv35" 33 | 34 | result = [ 35 | ast.subst_map(base_glyphs, target_suffix=suf_cv35), 36 | ast.subst_map( 37 | base_glyphs, 38 | source_suffix=suf_cv04, 39 | target_suffix=suf_cv35, 40 | ), 41 | ] 42 | 43 | for liga, suf in overwrite_glyphs.items(): 44 | result.extend( 45 | [ 46 | # overwrite 47 | ast.subst_map(f"{liga}{suf}", target_suffix=suf_cv35), 48 | ast.subst_map( 49 | liga, 50 | source_suffix=f"{suf_cv04}{suf}", 51 | target_suffix=f"{suf}{suf_cv35}", 52 | ), 53 | ] 54 | ) 55 | 56 | result += ast.subst_map(GLYPHS_ONE, source_suffix=".cv04") 57 | 58 | return result 59 | 60 | 61 | cv35_name = "Alternative Italic `l` without center tail" 62 | cv35_feat_italic = ast.CharacterVariant( 63 | id=35, desc=cv35_name, content=cv35_subst(), version="7.0", example="l" 64 | ) 65 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv36.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def cv36_subst(): 5 | return ast.subst_map( 6 | ["x", ast.gly("xl"), ast.gly("xl", ".cv04"), ast.gly("xl", ".cv35")], 7 | target_suffix=".cv36", 8 | ) 9 | 10 | 11 | cv36_name = "Alternative Italic `x` without top and bottom tails" 12 | cv36_feat_italic = ast.CharacterVariant( 13 | id=36, desc=cv36_name, content=cv36_subst(), version="7.0", example="x" 14 | ) 15 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv37.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def cv37_subst(): 5 | return ast.subst_map( 6 | [ 7 | "y", 8 | "yacute", 9 | "ycircumflex", 10 | "ydieresis", 11 | "ydotbelow", 12 | "ygrave", 13 | "yhookabove", 14 | "ymacron", 15 | "ytilde", 16 | ], 17 | target_suffix=".cv37", 18 | ) 19 | 20 | 21 | cv37_name = "Alternative Italic `y` with straight intersection" 22 | cv37_feat_italic = ast.CharacterVariant( 23 | id=37, desc=cv37_name, content=cv37_subst(), version="7.0", example="y" 24 | ) 25 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv38.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_G 3 | 4 | 5 | # https://github.com/subframe7536/maple-font/issues/329 6 | def cv38_subst(): 7 | return ast.subst_map( 8 | GLYPHS_G, 9 | target_suffix=".cv38", 10 | ) 11 | 12 | 13 | cv38_name = "Alternative italic `g` in double story style" 14 | cv38_feat_italic = ast.CharacterVariant( 15 | id=38, desc=cv38_name, content=cv38_subst(), version="7.1", example="g" 16 | ) 17 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv39.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_I 3 | 4 | 5 | # https://github.com/subframe7536/maple-font/issues/324 6 | def cv39_subst(): 7 | def gen(*suffix_list: str): 8 | result: list[str] = [] 9 | 10 | for suf in suffix_list: 11 | result.append(ast.gly("il", suf)) 12 | result.append(ast.gly("ill", suf)) 13 | 14 | return result 15 | 16 | return [ 17 | ast.subst_map( 18 | GLYPHS_I, 19 | target_suffix=".cv39", 20 | ), 21 | ast.subst_map( 22 | GLYPHS_I, 23 | source_suffix=".cv33", 24 | target_suffix=".cv39", 25 | ), 26 | ast.subst_map( 27 | gen("", ".cv04", ".cv33", ".cv04.cv33", ".cv33.cv35"), 28 | target_suffix=".cv39", 29 | ), 30 | ] 31 | 32 | 33 | cv39_name = "Alternative Italic `i` without bottom bar" 34 | cv39_feat_italic = ast.CharacterVariant( 35 | id=39, desc=cv39_name, content=cv39_subst(), version="7.1", example="i" 36 | ) 37 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv40.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_J_UPPER 3 | 4 | 5 | # https://github.com/subframe7536/maple-font/issues/324 6 | def cv40_subst(): 7 | return [ 8 | ast.subst_map( 9 | GLYPHS_J_UPPER, 10 | target_suffix=".cv40", 11 | ), 12 | ] 13 | 14 | 15 | cv40_name = "Alternative italic `J` without top bar" 16 | cv40_feat_italic = ast.CharacterVariant( 17 | id=40, desc=cv40_name, content=cv40_subst(), version="7.1", example="J" 18 | ) 19 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv41.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.cv._common import GLYPHS_R 3 | 4 | 5 | # https://github.com/subframe7536/maple-font/issues/328 6 | def cv41_subst(): 7 | return [ 8 | ast.subst_map( 9 | GLYPHS_R, 10 | target_suffix=".cv41", 11 | ), 12 | ] 13 | 14 | 15 | cv41_name = "Alternative italic `r` with bottom bar" 16 | cv41_feat_italic = ast.CharacterVariant( 17 | id=41, desc=cv41_name, content=cv41_subst(), version="7.1", example="r" 18 | ) 19 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv61.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | # https://github.com/subframe7536/maple-font/issues/348 5 | def cv61_subst(): 6 | return ast.subst_map( 7 | [",", ";", ";;", ";;;", "questiongreek"], 8 | target_suffix=".cv61", 9 | ) 10 | 11 | 12 | cv61_name = "Alternative `,` and `;` with straight tail" 13 | cv61_feat_regular = cv61_feat_italic = ast.CharacterVariant( 14 | id=61, desc=cv61_name, content=cv61_subst(), version="7.1", example=",;" 15 | ) 16 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv62.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | # https://github.com/subframe7536/maple-font/issues/348 5 | def cv62_subst(): 6 | return ast.subst_map( 7 | ["?", "questiondown", "??", "???", "?:", ":?", ":?>", "?.", ".?", "#?"], 8 | target_suffix=".cv62", 9 | ) 10 | 11 | 12 | cv62_name = "Alternative `?` with larger openings" 13 | cv62_feat_regular = cv62_feat_italic = ast.CharacterVariant( 14 | id=62, desc=cv62_name, content=cv62_subst(), version="7.1", example="?" 15 | ) 16 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv63.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def cv63_subst(): 5 | return [ 6 | ast.subst_map( 7 | "<=", 8 | target_suffix=".cv63", 9 | ), 10 | ] 11 | 12 | 13 | cv63_name = "Alternative `<=` in arrow style" 14 | cv63_feat_regular = cv63_feat_italic = ast.CharacterVariant( 15 | id=63, desc=cv63_name, content=cv63_subst(), version="7.1", example="<=" 16 | ) 17 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv64.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def cv64_subst(): 5 | return [ 6 | ast.subst_map( 7 | ["<=", ">="], 8 | target_suffix=".cv64", 9 | ), 10 | ] 11 | 12 | 13 | cv64_name = "Alternative `<=` and `>=` with horizen bottom bar" 14 | cv64_feat_regular = cv64_feat_italic = ast.CharacterVariant( 15 | id=64, desc=cv64_name, content=cv64_subst(), version="7.3", example="<=" 16 | ) 17 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv65.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def cv65_subst(): 5 | glyphs = ["&", "&&", "&&&"] 6 | return [ 7 | ast.subst_map( 8 | glyphs, 9 | target_suffix=".cv65", 10 | ), 11 | ast.subst_map( 12 | glyphs, 13 | source_suffix=".cv01", 14 | target_suffix=".cv65", 15 | ), 16 | ] 17 | 18 | 19 | cv65_name = "Alternative `&` in handwriting style" 20 | cv65_feat_regular = cv65_feat_italic = ast.CharacterVariant( 21 | id=65, desc=cv65_name, content=cv65_subst(), version="7.3", example="&" 22 | ) 23 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv96.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def cv96_subst(): 5 | return ast.subst_map( 6 | [ 7 | "“", 8 | "”", 9 | "‘", 10 | "’", 11 | ], 12 | target_suffix=".full", 13 | ) 14 | 15 | 16 | cv96_name = "Full width quotes (`“` / `”` / `‘` / `’`)" 17 | cv96_feat_cn = ast.CharacterVariant( 18 | id=96, desc=cv96_name, content=cv96_subst(), version="7.0", example="“‘’”" 19 | ) 20 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv97.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def cv97_subst(): 5 | return ast.subst_map( 6 | "…", 7 | target_suffix=".full", 8 | ) 9 | 10 | 11 | cv97_name = "Full width ellipsis (`…`)" 12 | cv97_feat_cn = ast.CharacterVariant( 13 | id=97, desc=cv97_name, content=cv97_subst(), version="7.0", example="……" 14 | ) 15 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv98.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def cv98_subst(): 5 | return ast.subst_map( 6 | "—", 7 | target_suffix=".full", 8 | ) 9 | 10 | 11 | cv98_name = "Full width emdash (`—`)" 12 | cv98_feat_cn = ast.CharacterVariant( 13 | id=98, desc=cv98_name, content=cv98_subst(), version="7.0", example="——" 14 | ) 15 | -------------------------------------------------------------------------------- /source/py/feature/cv/cv99.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.base.locl import lookup_tw 3 | 4 | 5 | def cv99_subst(): 6 | return [lookup_tw.use()] 7 | 8 | 9 | cv99_name = "Traditional centered punctuations" 10 | cv99_feat_cn = ast.CharacterVariant( 11 | id=99, desc=cv99_name, content=cv99_subst(), version="7.0", example=",。" 12 | ) 13 | -------------------------------------------------------------------------------- /source/py/feature/italic.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.calt import get_calt 3 | from source.py.feature.cv import ( 4 | cv01, 5 | cv04, 6 | cv31, 7 | cv32, 8 | cv33, 9 | cv34, 10 | cv35, 11 | cv36, 12 | cv37, 13 | cv38, 14 | cv39, 15 | cv40, 16 | cv41, 17 | cv61, 18 | cv62, 19 | cv63, 20 | cv64, 21 | cv65, 22 | ) 23 | from source.py.feature.ss import ( 24 | ss01, 25 | ss02, 26 | ss03, 27 | ss04, 28 | ss05, 29 | ss06, 30 | ss07, 31 | ss08, 32 | ss09, 33 | ss10, 34 | ss11, 35 | ) 36 | from source.py.feature.base.clazz import get_base_class_list, cls_digit 37 | 38 | 39 | cls_a = ast.Clazz("A", ["A", "a", "a.cv31"]) 40 | cls_b = ast.Clazz("B", ["B", "b"]) 41 | cls_c = ast.Clazz("C", ["C", "c"]) 42 | cls_d = ast.Clazz("D", ["D", "d"]) 43 | cls_e = ast.Clazz("E", ["E", "e"]) 44 | cls_f = ast.Clazz("F", ["F", "f", "f.cv32"]) 45 | cls_g = ast.Clazz("G", ["G", "g", "g.cv38"]) 46 | cls_h = ast.Clazz("H", ["H", "h"]) 47 | cls_i = ast.Clazz("I", ["I", "i", "i.cv33", "i.cv39"]) 48 | cls_j = ast.Clazz("J", ["J", "j", "j.cv33", "J.cv40"]) 49 | cls_k = ast.Clazz("K", ["K", "k", "k.cv34"]) 50 | cls_l = ast.Clazz("L", ["L", "l", "l.cv35"]) 51 | cls_m = ast.Clazz("M", ["M", "m"]) 52 | cls_n = ast.Clazz("N", ["N", "n"]) 53 | cls_o = ast.Clazz("O", ["O", "o"]) 54 | cls_p = ast.Clazz("P", ["P", "p"]) 55 | cls_q = ast.Clazz("Q", ["Q", "q", "Q.cv01"]) 56 | cls_r = ast.Clazz("R", ["R", "r"]) 57 | cls_s = ast.Clazz("S", ["S", "s"]) 58 | cls_t = ast.Clazz("T", ["T", "t"]) 59 | cls_u = ast.Clazz("U", ["U", "u"]) 60 | cls_v = ast.Clazz("V", ["V", "v"]) 61 | cls_w = ast.Clazz("W", ["W", "w"]) 62 | cls_x = ast.Clazz("X", ["X", "x", "x.cv36"]) 63 | cls_y = ast.Clazz("Y", ["Y", "y", "y.cv37"]) 64 | cls_z = ast.Clazz("Z", ["Z", "z"]) 65 | a_l = ast.Clazz( 66 | "AL", 67 | [ 68 | ast.gly("al"), 69 | ast.gly("al", ".cv31"), 70 | ast.gly("al", ".cv35"), 71 | ast.gly("al", ".cv31.cv35"), 72 | ast.gly("al", ".cv04"), 73 | ast.gly("al", ".cv04.cv31"), 74 | ast.gly("al", ".ss06"), 75 | ], 76 | ) 77 | 78 | cls_letters_list = [ 79 | cls_a, 80 | cls_b, 81 | cls_c, 82 | cls_d, 83 | cls_e, 84 | cls_f, 85 | cls_g, 86 | cls_h, 87 | cls_i, 88 | cls_j, 89 | cls_k, 90 | cls_l, 91 | cls_m, 92 | cls_n, 93 | cls_o, 94 | cls_p, 95 | cls_q, 96 | cls_r, 97 | cls_s, 98 | cls_t, 99 | cls_u, 100 | cls_v, 101 | cls_w, 102 | cls_x, 103 | cls_y, 104 | cls_z, 105 | ] 106 | 107 | cls_var = ast.Clazz("Var", ["_", "__", *cls_letters_list, cls_digit]) 108 | cls_hex_letter = ast.Clazz("HexLetter", [cls_a, cls_b, cls_c, cls_d, cls_e, cls_f]) 109 | 110 | class_list_italic = [ 111 | *get_base_class_list(), 112 | *cls_letters_list, 113 | a_l, 114 | cls_var, 115 | cls_hex_letter, 116 | ] 117 | 118 | 119 | calt_italic = get_calt(cls_var, cls_hex_letter, is_italic=True) 120 | 121 | cv_list_italic = [ 122 | cv01.cv01_feat_italic, 123 | cv04.cv04_feat_italic, 124 | cv31.cv31_feat_italic, 125 | cv32.cv32_feat_italic, 126 | cv33.cv33_feat_italic, 127 | cv34.cv34_feat_italic, 128 | cv35.cv35_feat_italic, 129 | cv36.cv36_feat_italic, 130 | cv37.cv37_feat_italic, 131 | cv38.cv38_feat_italic, 132 | cv39.cv39_feat_italic, 133 | cv40.cv40_feat_italic, 134 | cv41.cv41_feat_italic, 135 | cv61.cv61_feat_italic, 136 | cv62.cv62_feat_italic, 137 | cv63.cv63_feat_italic, 138 | cv64.cv64_feat_italic, 139 | cv65.cv65_feat_italic, 140 | ] 141 | 142 | ss_list_italic = [ 143 | ss01.ss01_feat, 144 | ss02.ss02_feat, 145 | ss03.ss03_feat, 146 | ss04.ss04_feat, 147 | ss05.ss05_feat, 148 | ss06.ss06_feat, 149 | ss07.ss07_feat, 150 | ss08.ss08_feat, 151 | ss09.ss09_feat, 152 | ss10.ss10_feat, 153 | ss11.ss11_feat, 154 | ] 155 | -------------------------------------------------------------------------------- /source/py/feature/regular.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | from source.py.feature.base.clazz import get_base_class_list, cls_digit 3 | from source.py.feature.cv import ( 4 | cv01, 5 | cv02, 6 | cv03, 7 | cv04, 8 | cv05, 9 | cv06, 10 | cv07, 11 | cv08, 12 | cv61, 13 | cv62, 14 | cv63, 15 | cv64, 16 | cv65, 17 | ) 18 | from source.py.feature.ss import ( 19 | ss01, 20 | ss02, 21 | ss03, 22 | ss04, 23 | ss05, 24 | ss07, 25 | ss08, 26 | ss09, 27 | ss10, 28 | ss11, 29 | ) 30 | 31 | cls_a = ast.Clazz("A", ["A", "a", "a.cv02"]) 32 | cls_b = ast.Clazz("B", ["B", "b"]) 33 | cls_c = ast.Clazz("C", ["C", "c"]) 34 | cls_d = ast.Clazz("D", ["D", "d"]) 35 | cls_e = ast.Clazz("E", ["E", "e"]) 36 | cls_f = ast.Clazz("F", ["F", "f"]) 37 | cls_g = ast.Clazz("G", ["G", "g", "g.cv05"]) 38 | cls_h = ast.Clazz("H", ["H", "h"]) 39 | cls_i = ast.Clazz("I", ["I", "i", "i.cv03", "i.cv06"]) 40 | cls_j = ast.Clazz("J", ["J", "j", "J.cv07"]) 41 | cls_k = ast.Clazz("K", ["K", "k"]) 42 | cls_l = ast.Clazz("L", ["L", "l", "l.cv04"]) 43 | cls_m = ast.Clazz("M", ["M", "m"]) 44 | cls_n = ast.Clazz("N", ["N", "n"]) 45 | cls_o = ast.Clazz("O", ["O", "o"]) 46 | cls_p = ast.Clazz("P", ["P", "p"]) 47 | cls_q = ast.Clazz("Q", ["Q", "q", "Q.cv01"]) 48 | cls_r = ast.Clazz("R", ["R", "r"]) 49 | cls_s = ast.Clazz("S", ["S", "s"]) 50 | cls_t = ast.Clazz("T", ["T", "t"]) 51 | cls_u = ast.Clazz("U", ["U", "u"]) 52 | cls_v = ast.Clazz("V", ["V", "v"]) 53 | cls_w = ast.Clazz("W", ["W", "w"]) 54 | cls_x = ast.Clazz("X", ["X", "x"]) 55 | cls_y = ast.Clazz("Y", ["Y", "y"]) 56 | cls_z = ast.Clazz("Z", ["Z", "z"]) 57 | 58 | cls_letters_list = [ 59 | cls_a, 60 | cls_b, 61 | cls_c, 62 | cls_d, 63 | cls_e, 64 | cls_f, 65 | cls_g, 66 | cls_h, 67 | cls_i, 68 | cls_j, 69 | cls_k, 70 | cls_l, 71 | cls_m, 72 | cls_n, 73 | cls_o, 74 | cls_p, 75 | cls_q, 76 | cls_r, 77 | cls_s, 78 | cls_t, 79 | cls_u, 80 | cls_v, 81 | cls_w, 82 | cls_x, 83 | cls_y, 84 | cls_z, 85 | ] 86 | 87 | cls_var = ast.Clazz("Var", ["_", "__", *cls_letters_list, cls_digit]) 88 | cls_hex_letter = ast.Clazz("HexLetter", [cls_a, cls_b, cls_c, cls_d, cls_e, cls_f]) 89 | 90 | class_list_regular = [ 91 | *get_base_class_list(), 92 | *cls_letters_list, 93 | cls_var, 94 | cls_hex_letter, 95 | ] 96 | 97 | cv_list_regular = [ 98 | cv01.cv01_feat_regular, 99 | cv02.cv02_feat_regular, 100 | cv03.cv03_feat_regular, 101 | cv04.cv04_feat_regular, 102 | cv05.cv05_feat_regular, 103 | cv06.cv06_feat_regular, 104 | cv07.cv07_feat_regular, 105 | cv08.cv08_feat_regular, 106 | cv61.cv61_feat_regular, 107 | cv62.cv62_feat_regular, 108 | cv63.cv63_feat_regular, 109 | cv64.cv64_feat_regular, 110 | cv65.cv65_feat_regular, 111 | ] 112 | 113 | 114 | ss_list_regular = [ 115 | ss01.ss01_feat, 116 | ss02.ss02_feat, 117 | ss03.ss03_feat, 118 | ss04.ss04_feat, 119 | ss05.ss05_feat, 120 | ss07.ss07_feat, 121 | ss08.ss08_feat, 122 | ss09.ss09_feat, 123 | ss10.ss10_feat, 124 | ss11.ss11_feat, 125 | ] 126 | -------------------------------------------------------------------------------- /source/py/feature/ss/ss01.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | 3 | 4 | def ss01_subst(): 5 | return ast.subst_map( 6 | [ 7 | "==", 8 | "===", 9 | "!=", 10 | "!==", 11 | "=/=", 12 | ], 13 | target_suffix=".ss01", 14 | ) 15 | 16 | 17 | ss01_name = "Broken multiple equals ligatures (`==`, `===`, `!=`, `!==` ...)" 18 | ss01_feat = ast.StylisticSet( 19 | id=1, desc=ss01_name, content=ss01_subst(), version="7.0", sample="==" 20 | ) 21 | -------------------------------------------------------------------------------- /source/py/feature/ss/ss02.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | 3 | 4 | def ss02_subst(): 5 | return ast.subst_map( 6 | [ 7 | "<=", 8 | ">=", 9 | ], 10 | target_suffix=".ss02", 11 | ) 12 | 13 | 14 | ss02_name = "Broken compare and equal ligatures (`<=`, `>=`)" 15 | ss02_feat = ast.StylisticSet( 16 | id=2, desc=ss02_name, content=ss02_subst(), version="7.0", sample=">=" 17 | ) 18 | -------------------------------------------------------------------------------- /source/py/feature/ss/ss03.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.calt.tag import built_in_tag_text 3 | 4 | 5 | def ss03_subst(): 6 | # There are many classes for letters, allows to use letters in any case 7 | # e.g. `@I @N @F @O` matches: 8 | # - INFO 9 | # - INFo 10 | # - INfO 11 | # - INfo 12 | # - InFO 13 | # - InFo 14 | # - InfO 15 | # - Info 16 | # - iNFO 17 | # - iNFo 18 | # - iNfO 19 | # - iNfo 20 | # - inFO 21 | # - inFo 22 | # - infO 23 | # - info 24 | result = [] 25 | for text in built_in_tag_text: 26 | arr = ["["] + [f"@{g.upper()}" for g in text] + ["]"] 27 | result.append( 28 | ast.subst_liga( 29 | arr, 30 | target=f"tag_{text}.liga", 31 | lookup_name=f"tag_{text}.liga.ss03", 32 | desc=f"[{text}]", 33 | ) 34 | ) 35 | return result 36 | 37 | 38 | ss03_name = "Allow to use any case in all tags" 39 | ss03_feat = ast.StylisticSet( 40 | id=3, desc=ss03_name, content=ss03_subst(), version="7.0", sample="[info]" 41 | ) 42 | -------------------------------------------------------------------------------- /source/py/feature/ss/ss04.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | 3 | 4 | def ss04_subst(): 5 | return ast.subst_map( 6 | [ 7 | "__", 8 | "#__", 9 | ], 10 | target_suffix=".ss04", 11 | ) 12 | 13 | 14 | ss04_name = "Broken multiple underscores ligatures (`__`, `#__`)" 15 | ss04_feat = ast.StylisticSet( 16 | id=4, desc=ss04_name, content=ss04_subst(), version="7.0", sample="__" 17 | ) 18 | -------------------------------------------------------------------------------- /source/py/feature/ss/ss05.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | 3 | 4 | def ss05_subst(): 5 | return [ast.subst_map("\\", source_suffix=".liga")] 6 | 7 | 8 | ss05_name = 'Revert thin backslash in escape symbols (`\\\\`, `\\"`, `\\.` ...)' 9 | ss05_feat = ast.StylisticSet( 10 | id=5, desc=ss05_name, content=ss05_subst(), version="7.0", sample="\\\\" 11 | ) 12 | -------------------------------------------------------------------------------- /source/py/feature/ss/ss06.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | 3 | 4 | def ss06_subst(): 5 | # Only handle glyphs that contains: 6 | # - default letter & default `l` 7 | # - default `ll` 8 | # - `ff` 9 | # - `tt` 10 | return ast.subst_map( 11 | [ 12 | ast.gly("Cl"), 13 | ast.gly("al"), 14 | ast.gly("cl"), 15 | ast.gly("el"), 16 | ast.gly("il"), 17 | ast.gly("ll"), 18 | ast.gly("tl"), 19 | ast.gly("ul"), 20 | ast.gly("xl"), 21 | ast.gly("all"), 22 | ast.gly("all", ".cv31"), 23 | ast.gly("ell"), 24 | ast.gly("ill"), 25 | ast.gly("ill", ".cv33"), 26 | ast.gly("ill", ".cv39"), 27 | ast.gly("ill", ".cv33.cv39"), 28 | ast.gly("ull"), 29 | ast.gly("ff"), 30 | ast.gly("ff", ".cv32"), 31 | ast.gly("tt"), 32 | ], 33 | target_suffix=".ss06", 34 | ) 35 | 36 | 37 | ss06_name = "Break connected strokes between italic letters (`al`, `il`, `ull` ...)" 38 | ss06_feat = ast.StylisticSet( 39 | id=6, desc=ss06_name, content=ss06_subst(), version="7.0", sample="all" 40 | ) 41 | -------------------------------------------------------------------------------- /source/py/feature/ss/ss07.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | 3 | 4 | def ss07_subst(): 5 | return [ 6 | ast.subst_liga( 7 | ">>", 8 | lookup_name=f"relax_{ast.gly('>>')}", 9 | ign_prefix=ast.cls(">", "/", "<"), 10 | ign_suffix=">", 11 | ), 12 | ast.subst_liga( 13 | ">>>", 14 | lookup_name=f"relax_{ast.gly('>>>')}", 15 | ign_prefix=">", 16 | ign_suffix=">", 17 | ), 18 | ] 19 | 20 | 21 | ss07_name = "Relax the conditions for multiple greaters ligatures (`>>` or `>>>`)" 22 | ss07_feat = ast.StylisticSet( 23 | id=7, desc=ss07_name, content=ss07_subst(), version="7.0", sample=">>>" 24 | ) 25 | -------------------------------------------------------------------------------- /source/py/feature/ss/ss08.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | from source.py.feature.base.clazz import cls_question 3 | 4 | 5 | def ss08_subst(): 6 | return [ 7 | ast.subst_liga( 8 | "<<-", 9 | target=ast.gly("<<-", ".ss08"), 10 | ign_prefix=ast.cls("<", "-"), 11 | ign_suffix=ast.cls("<", ">", "-"), 12 | extra_rules=[ 13 | ast.subst(ast.SPC, ast.gly("<<"), "-", ast.SPC), 14 | ], 15 | ), 16 | ast.subst_liga( 17 | ">>-", 18 | target=ast.gly(">>-", ".ss08"), 19 | ign_prefix=ast.cls(">", "-"), 20 | ign_suffix=ast.cls("-", ">", "<"), 21 | extra_rules=[ 22 | ast.subst(ast.SPC, ast.gly(">>"), "-", ast.SPC), 23 | ], 24 | ), 25 | ast.subst_liga( 26 | "<<=", 27 | target=ast.gly("<<=", ".ss08"), 28 | ign_prefix=ast.cls("=", "<"), 29 | ign_suffix=ast.cls("=", ">", "<"), 30 | extra_rules=[ 31 | ast.subst(ast.SPC, ast.gly("<<"), "=", ast.SPC), 32 | ], 33 | ), 34 | ast.subst_liga( 35 | ">>=", 36 | target=ast.gly(">>=", ".ss08"), 37 | ign_prefix=ast.cls(">", "="), 38 | ign_suffix=ast.cls("=", ">", "<"), 39 | extra_rules=[ 40 | ast.subst(ast.SPC, ast.gly(">>"), "=", ast.SPC), 41 | ], 42 | ), 43 | ast.subst_liga( 44 | "-<<", 45 | target=ast.gly("-<<", ".ss08"), 46 | ign_prefix=ast.cls("-", "<", ">"), 47 | ign_suffix=ast.cls("<", "-"), 48 | extra_rules=[ 49 | ast.subst( 50 | [ast.SPC, ast.SPC], 51 | ast.gly("<<"), 52 | None, 53 | ast.gly("-<<", ".ss08"), 54 | ), 55 | ast.subst(None, "-", [ast.SPC, ast.gly("<<")], ast.SPC), 56 | ], 57 | ), 58 | ast.subst_liga( 59 | "->>", 60 | target=ast.gly("->>", ".ss08"), 61 | ign_prefix=ast.cls("-", ">", "<"), 62 | ign_suffix=ast.cls(">", "-"), 63 | extra_rules=[ 64 | ast.subst( 65 | [ast.SPC, ast.SPC], 66 | ast.gly(">>"), 67 | None, 68 | ast.gly("->>", ".ss08"), 69 | ), 70 | ast.subst(None, "-", [ast.SPC, ast.gly(">>")], ast.SPC), 71 | ], 72 | ), 73 | ast.subst_liga( 74 | "=<<", 75 | target=ast.gly("=<<", ".ss08"), 76 | ign_prefix=ast.cls("=", "<", ">"), 77 | ign_suffix=ast.cls("<", "="), 78 | extra_rules=[ 79 | ast.ign(["(", cls_question], "=", ["<", "<"]), 80 | ast.subst( 81 | [ast.SPC, ast.SPC], 82 | ast.gly("<<"), 83 | None, 84 | ast.gly("=<<", ".ss08"), 85 | ), 86 | ast.subst(None, "=", [ast.SPC, ast.gly("<<")], ast.SPC), 87 | ], 88 | ), 89 | ast.subst_liga( 90 | "=>>", 91 | target=ast.gly("=>>", ".ss08"), 92 | ign_prefix=ast.cls("=", ">", "<"), 93 | ign_suffix=ast.cls(">", "="), 94 | extra_rules=[ 95 | ast.ign(["(", cls_question], "=", [">", ">"]), 96 | ast.subst( 97 | [ast.SPC, ast.SPC], 98 | ast.gly(">>"), 99 | None, 100 | ast.gly("=>>", ".ss08"), 101 | ), 102 | ast.subst(None, "=", [ast.SPC, ast.gly(">>")], ast.SPC), 103 | ], 104 | ), 105 | ] 106 | 107 | 108 | ss08_name = ( 109 | "Double headed arrows and reverse arrows ligatures (`>>=`, `-<<`, `->>`, `>>-` ...)" 110 | ) 111 | ss08_feat = ast.StylisticSet( 112 | id=8, desc=ss08_name, content=ss08_subst(), version="7.0", sample=">>=" 113 | ) 114 | -------------------------------------------------------------------------------- /source/py/feature/ss/ss09.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | # https://github.com/subframe7536/maple-font/issues/352 5 | def ss09_subst(): 6 | return ast.subst_liga( 7 | "~=", # Lua 8 | target=ast.gly("~=", ".ss09"), 9 | ign_prefix=ast.cls("~", "<", "="), 10 | ign_suffix=ast.cls("~", "=", ">", "<", ":"), 11 | ) 12 | 13 | 14 | ss09_name = "Asciitilde equal as not equal to ligature (`~=`)" 15 | ss09_feat = ast.StylisticSet( 16 | id=9, desc=ss09_name, content=ss09_subst(), version="7.1", sample="~=" 17 | ) 18 | -------------------------------------------------------------------------------- /source/py/feature/ss/ss10.py: -------------------------------------------------------------------------------- 1 | import source.py.feature.ast as ast 2 | 3 | 4 | def ss10_subst(): 5 | return [ 6 | ast.subst_liga( 7 | "=~", 8 | target=ast.gly("=~", ".ss10"), 9 | ign_prefix=ast.cls("~", "<", ">", ":", "="), 10 | ign_suffix=ast.cls("~", "=", ">"), 11 | ), 12 | ast.subst_liga( 13 | "!~", 14 | target=ast.gly("!~", ".ss10"), 15 | ign_prefix="!", 16 | ign_suffix=ast.cls("!", "~", "=", ">"), 17 | ), 18 | ] 19 | 20 | 21 | ss10_name = ( 22 | "Approximately equal to and approximately not equal to ligatures (`=~`, `!~`)" 23 | ) 24 | ss10_feat = ast.StylisticSet( 25 | id=10, desc=ss10_name, content=ss10_subst(), version="7.1", sample="=~" 26 | ) 27 | -------------------------------------------------------------------------------- /source/py/feature/ss/ss11.py: -------------------------------------------------------------------------------- 1 | from source.py.feature import ast 2 | 3 | 4 | def ss11_subst(): 5 | cls_ign_equal = ast.Clazz("IgnoreEqual", [">", "=", ":"]) 6 | ampersand_cv01 = ast.gly("&", ".cv01") 7 | question_cv62 = ast.gly("?", ".cv62") 8 | ampersand_cv65 = ast.gly("&", ".cv65") 9 | return [ 10 | ast.cls_states(cls_ign_equal), 11 | ast.Lookup( 12 | "bars_equal", 13 | "|=", 14 | ast.subst_map(["|="], target_suffix=".ss11"), 15 | ), 16 | ast.subst_liga( 17 | "||=", 18 | target=ast.gly("||=", ".ss11"), 19 | ign_prefix="|", 20 | ign_suffix="|", 21 | extra_rules=[ 22 | ast.subst(ast.SPC, ast.gly("||"), "=", ast.SPC) 23 | ] 24 | ), 25 | ast.subst_liga( 26 | "/=", 27 | target=ast.gly("/=", ".ss11"), 28 | ign_prefix=ast.cls("/", "<"), 29 | ign_suffix=cls_ign_equal, 30 | ), 31 | ast.subst_liga( 32 | "//=", 33 | target=ast.gly("//=", ".ss11"), 34 | ign_prefix=ast.cls("/", "<"), 35 | ign_suffix=cls_ign_equal, 36 | extra_rules=[ast.subst(ast.SPC, ast.gly("//"), "=", ast.SPC)], 37 | ), 38 | ast.subst_liga( 39 | "^=", 40 | target=ast.gly("^=", ".ss11"), 41 | ign_prefix="^", 42 | ign_suffix=cls_ign_equal, 43 | ), 44 | ast.subst_liga( 45 | "&=", 46 | target=ast.gly("&=", ".ss11"), 47 | ign_prefix="&", 48 | ign_suffix=cls_ign_equal, 49 | ), 50 | ast.subst_liga( 51 | [ampersand_cv01, "="], 52 | target=ast.gly("&=", ".cv01.ss11"), 53 | desc="&= in cv01", 54 | ign_prefix=ampersand_cv01, 55 | ign_suffix=cls_ign_equal, 56 | ), 57 | ast.subst_liga( 58 | [ampersand_cv65, "="], 59 | target=ast.gly("&=", ".cv65.ss11"), 60 | desc="&= in cv65", 61 | ign_prefix=ampersand_cv65, 62 | ign_suffix=cls_ign_equal, 63 | ), 64 | ast.subst_liga( 65 | "&&=", 66 | target=ast.gly("&&=", ".ss11"), 67 | ign_prefix="&", 68 | ign_suffix=cls_ign_equal, 69 | extra_rules=[ast.subst(ast.SPC, ast.gly("&&"), "=", ast.SPC)], 70 | ), 71 | ast.subst_liga( 72 | [ampersand_cv01, ampersand_cv01, "="], 73 | target=ast.gly("&&=", ".cv01.ss11"), 74 | desc="&&= in cv01", 75 | ign_prefix=ampersand_cv01, 76 | ign_suffix=cls_ign_equal, 77 | extra_rules=[ 78 | ast.subst( 79 | ast.SPC, 80 | ast.gly("&&", ".cv01"), 81 | "=", 82 | ast.SPC, 83 | ) 84 | ], 85 | ), 86 | ast.subst_liga( 87 | [ampersand_cv65, ampersand_cv65, "="], 88 | target=ast.gly("&&=", ".cv65.ss11"), 89 | desc="&&= in cv65", 90 | ign_prefix=ampersand_cv65, 91 | ign_suffix=cls_ign_equal, 92 | extra_rules=[ 93 | ast.subst( 94 | ast.SPC, 95 | ast.gly("&&", ".cv65"), 96 | "=", 97 | ast.SPC, 98 | ) 99 | ], 100 | ), 101 | ast.subst_liga( 102 | "?=", 103 | target=ast.gly("?=", ".ss11"), 104 | ign_prefix="?", 105 | ign_suffix=cls_ign_equal, 106 | ), 107 | ast.subst_liga( 108 | [question_cv62, "="], 109 | target=ast.gly("?=", ".cv62.ss11"), 110 | desc="?= in cv62", 111 | ign_prefix=question_cv62, 112 | ign_suffix=cls_ign_equal, 113 | ), 114 | ast.subst_liga( 115 | "??=", 116 | target=ast.gly("??=", ".ss11"), 117 | ign_prefix="?", 118 | ign_suffix=cls_ign_equal, 119 | extra_rules=[ast.subst(ast.SPC, ast.gly("??"), "=", ast.SPC)], 120 | ), 121 | ast.subst_liga( 122 | [question_cv62, question_cv62, "="], 123 | target=ast.gly("??=", ".cv62.ss11"), 124 | desc="??= in cv62", 125 | ign_prefix=question_cv62, 126 | ign_suffix=cls_ign_equal, 127 | extra_rules=[ast.subst(ast.SPC, ast.gly("??", ".cv62"), "=", ast.SPC)], 128 | ), 129 | ] 130 | 131 | 132 | ss11_name = "Equal and extra punctuation ligatures (`|=`, `/=`, `?=`, `&=`, ...)" 133 | ss11_feat = ast.StylisticSet( 134 | id=11, desc=ss11_name, content=ss11_subst(), version="7.1", sample="|=" 135 | ) 136 | -------------------------------------------------------------------------------- /source/py/freeze.py: -------------------------------------------------------------------------------- 1 | def is_enable(v): 2 | return v.upper().startswith("ENABLE") 3 | 4 | 5 | def is_disable(v): 6 | return v.upper().startswith("DISABLE") 7 | 8 | 9 | def is_ignore(v): 10 | return v.upper().startswith("IGNORE") 11 | 12 | 13 | def get_freeze_config_str(feature_freeze, enable_liga): 14 | invalid_items = [] 15 | 16 | result = "" 17 | for k, v in feature_freeze.items(): 18 | if isinstance(v, str): 19 | if is_enable(v): 20 | result += f"+{k};" 21 | elif is_disable(v): 22 | result += f"-{k};" 23 | elif not is_ignore(v): 24 | invalid_items.append((k, v)) 25 | else: 26 | invalid_items.append((k, v)) 27 | 28 | if len(invalid_items) > 0: 29 | report = ", ".join([f"{k}: {v}" for k, v in invalid_items]) 30 | raise TypeError(f"Invalid freeze config item: {{ {report} }}") 31 | 32 | if not enable_liga: 33 | result += "-calt;" 34 | return result 35 | 36 | 37 | def freeze_feature(font, calt, moving_rules=[], config={}): 38 | # check feature list 39 | feature_record = font["GSUB"].table.FeatureList.FeatureRecord 40 | feature_dict = { 41 | feature.FeatureTag: feature.Feature 42 | for feature in feature_record 43 | if feature.FeatureTag != "calt" 44 | } 45 | 46 | calt_features = [] 47 | if calt: 48 | calt_features = [ 49 | feature.Feature 50 | for feature in feature_record 51 | if feature.FeatureTag == "calt" 52 | ] 53 | else: 54 | for feature in feature_record: 55 | if feature.FeatureTag == "calt": 56 | feature.Feature.LookupListIndex.clear() 57 | feature.Feature.LookupCount = 0 58 | feature.FeatureTag = "DELT" 59 | 60 | # Process features 61 | for tag, status in config.items(): 62 | target_feature = feature_dict.get(tag) 63 | if not target_feature or is_ignore(status): 64 | continue 65 | 66 | if is_disable(status): 67 | target_feature.LookupListIndex = [] 68 | continue 69 | 70 | if tag in moving_rules and calt: 71 | # Enable by moving rules into "calt" 72 | for calt_feat in calt_features: 73 | calt_feat.LookupListIndex.extend(target_feature.LookupListIndex) 74 | else: 75 | # Enable by replacing data in glyf and hmtx tables 76 | glyph_dict = font["glyf"].glyphs 77 | hmtx_dict = font["hmtx"].metrics 78 | for index in target_feature.LookupListIndex: 79 | lookup_subtable = ( 80 | font["GSUB"].table.LookupList.Lookup[index].SubTable[0] 81 | ) 82 | if not lookup_subtable or "mapping" not in lookup_subtable.__dict__: 83 | continue 84 | for old_key, new_key in lookup_subtable.mapping.items(): 85 | if ( 86 | old_key in glyph_dict 87 | and old_key in hmtx_dict 88 | and new_key in glyph_dict 89 | and new_key in hmtx_dict 90 | ): 91 | glyph_dict[old_key] = glyph_dict[new_key] 92 | hmtx_dict[old_key] = hmtx_dict[new_key] 93 | else: 94 | print(f"{old_key} or {new_key} does not exist") 95 | return 96 | -------------------------------------------------------------------------------- /source/py/in_browser.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | from zipfile import ZipFile 4 | from fontTools.ttLib import TTFont 5 | 6 | MOVING_RULES = ["ss03", "ss07", "ss08", "ss09", "ss10", "ss11"] 7 | 8 | 9 | def get_freeze_config_str(config): 10 | result = "" 11 | for k, v in config.items(): 12 | if v == "1": 13 | result += f"+{k};" 14 | if v == "0" and k == "calt": 15 | result += "-calt;" 16 | return result 17 | 18 | 19 | def freeze_feature(font, moving_rules, config): 20 | calt = config.get("calt") == "1" 21 | feature_record = font["GSUB"].table.FeatureList.FeatureRecord 22 | feature_dict = { 23 | feature.FeatureTag: feature.Feature 24 | for feature in feature_record 25 | if feature.FeatureTag != "calt" 26 | } 27 | 28 | calt_features = [] 29 | if calt: 30 | calt_features = [ 31 | feature.Feature 32 | for feature in feature_record 33 | if feature.FeatureTag == "calt" 34 | ] 35 | else: 36 | for feature in feature_record: 37 | if feature.FeatureTag == "calt": 38 | feature.Feature.LookupListIndex.clear() 39 | feature.Feature.LookupCount = 0 40 | feature.FeatureTag = "DELT" 41 | 42 | for tag, status in config.items(): 43 | target_feature = feature_dict.get(tag) 44 | if not target_feature or status == "0": 45 | continue 46 | 47 | if tag in moving_rules and calt: 48 | for calt_feat in calt_features: 49 | calt_feat.LookupListIndex.extend(target_feature.LookupListIndex) 50 | else: 51 | glyph_dict = font["glyf"].glyphs 52 | hmtx_dict = font["hmtx"].metrics 53 | for index in target_feature.LookupListIndex: 54 | lookup_subtable = ( 55 | font["GSUB"].table.LookupList.Lookup[index].SubTable[0] 56 | ) 57 | if not lookup_subtable or "mapping" not in lookup_subtable.__dict__: 58 | continue 59 | for old_key, new_key in lookup_subtable.mapping.items(): 60 | if ( 61 | old_key in glyph_dict 62 | and old_key in hmtx_dict 63 | and new_key in glyph_dict 64 | and new_key in hmtx_dict 65 | ): 66 | glyph_dict[old_key] = glyph_dict[new_key] 67 | hmtx_dict[old_key] = hmtx_dict[new_key] 68 | 69 | 70 | def set_font_name(font, name: str, id: int): 71 | font["name"].setName(name, nameID=id, platformID=3, platEncID=1, langID=0x409) 72 | 73 | 74 | def get_font_name(font, id: int) -> str: 75 | return ( 76 | font["name"] 77 | .getName(nameID=id, platformID=3, platEncID=1, langID=0x409) 78 | .__str__() 79 | ) 80 | 81 | 82 | def main(zip_path: str, target_path: str, config: dict): 83 | with ( 84 | ZipFile(zip_path, "r") as zip_in, 85 | ZipFile(target_path, "w") as zip_out, 86 | ): 87 | for file_info in zip_in.infolist(): 88 | file_name = file_info.filename 89 | if file_name.lower().endswith(".ttf") or file_name.lower().endswith(".otf"): 90 | print(f"Patch: {file_name}") 91 | with zip_in.open(file_info) as ttf_file: 92 | ttf_buffer = ttf_file.read() 93 | ttf_io = io.BytesIO(ttf_buffer) 94 | font = TTFont(ttf_io) 95 | 96 | suffix = get_freeze_config_str(config) 97 | freeze_feature(font, MOVING_RULES, config) 98 | set_font_name(font, get_font_name(font, 3) + suffix, 3) 99 | 100 | output_io = io.BytesIO() 101 | font.save(output_io) 102 | zip_out.writestr(file_info, output_io.getvalue()) 103 | font.close() 104 | else: 105 | print(f'Skip: {file_name}') 106 | zip_out.writestr(file_info, zip_in.read(file_info)) 107 | 108 | zip_out.writestr( 109 | "patch-in-browser.json", 110 | json.dumps( 111 | {k: "freeze" if v == "1" else "ignore" for k, v in config.items()}, 112 | indent=4, 113 | ), 114 | ) 115 | print("Write: patch-in-browser.json") 116 | 117 | print("Repack zip") 118 | -------------------------------------------------------------------------------- /source/py/task/_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def write_text(file_path: str, content: str, mode: str = "w") -> None: 5 | if not isinstance(file_path, str) or not file_path: 6 | raise ValueError("Invalid file path") 7 | if not isinstance(content, str): 8 | raise ValueError("Invalid content") 9 | with open(file_path, encoding="utf-8", mode=mode, newline="\n") as file: 10 | file.write(content) 11 | 12 | 13 | def write_json(file_path: str, data: dict) -> None: 14 | with open(file_path, "w", encoding="utf-8", newline="\n") as file: 15 | json.dump(data, file, indent=2) 16 | 17 | 18 | def read_json(file_path: str) -> dict: 19 | with open(file_path, "r", encoding="utf-8") as file: 20 | return json.load(file) 21 | 22 | 23 | def read_text(file_path: str) -> str: 24 | with open(file_path, "r", encoding="utf-8") as file: 25 | return file.read() 26 | -------------------------------------------------------------------------------- /source/py/task/fea.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from source.py.feature import ( 4 | generate_fea_string, 5 | generate_fea_string_cn_only, 6 | get_all_calt_text, 7 | get_cv_desc, 8 | get_cv_italic_desc, 9 | get_cv_cn_desc, 10 | get_freeze_moving_rules, 11 | get_ss_desc, 12 | get_total_feat_dict, 13 | ) 14 | from source.py.feature import normal_enabled_features 15 | from source.py.task._utils import read_json, read_text, write_json, write_text 16 | from source.py.utils import joinPaths 17 | 18 | 19 | def replace_section(md_path: str, border: str, content: str) -> None: 20 | md_content = read_text(md_path) 21 | pattern = f"{border}(.*){border}" 22 | updated_content = re.sub( 23 | pattern, f"{border}\n{content}\n{border}", md_content, flags=re.DOTALL 24 | ) 25 | write_text(md_path, updated_content) 26 | 27 | 28 | def update_feature_freeze( 29 | file_path: str, 30 | features: dict[str, str], 31 | ) -> None: 32 | config = read_json(file_path) 33 | config["feature_freeze"] = {tag: "ignore" for tag in features} 34 | write_json(file_path, config) 35 | 36 | 37 | def update_schema(file_path: str, features: dict[str, str]) -> None: 38 | schema = read_json(file_path) 39 | schema["properties"]["feature_freeze"]["properties"] = { 40 | tag: {"description": desc, "$ref": "#/definitions/freeze_options"} 41 | for tag, desc in features.items() 42 | } 43 | write_json(file_path, schema) 44 | 45 | 46 | def fea(output: str, cn: bool) -> None: 47 | # Generate feature files 48 | banner = "Auto generated by `python task.py fea`" 49 | files = { 50 | "regular.fea": generate_fea_string(False, False), 51 | "italic.fea": generate_fea_string(True, False), 52 | "cn.fea": generate_fea_string_cn_only(), 53 | } 54 | for filename, content in files.items(): 55 | write_text(joinPaths(output, filename), f"# {banner}\n\n{content}") 56 | 57 | banner = banner.replace("fea`", "fea --cn`") 58 | files_cn = {} 59 | if cn: 60 | files_cn = { 61 | "regular_cn.fea": generate_fea_string(False, True), 62 | "italic_cn.fea": generate_fea_string(True, True), 63 | } 64 | for filename, content in files_cn.items(): 65 | fea_path = joinPaths(output, filename) 66 | if cn: 67 | write_text(fea_path, f"# {banner}\n\n{content}") 68 | else: 69 | try: 70 | os.remove(fea_path) 71 | except Exception: 72 | pass 73 | 74 | # Update README sections 75 | md_path = joinPaths(output, "README.md") 76 | sections = { 77 | "": get_all_calt_text(), 78 | "": get_cv_desc(), 79 | "": get_cv_italic_desc(), 80 | "": get_cv_cn_desc(), 81 | "": get_ss_desc(), 82 | } 83 | for border, content in sections.items(): 84 | replace_section(md_path, border, content) 85 | 86 | # Update configuration files 87 | features = get_total_feat_dict() 88 | 89 | update_schema(joinPaths("source", "schema.json"), features) 90 | update_feature_freeze("config.json", features) 91 | feat_str = ", ".join(normal_enabled_features) 92 | for f in ["README.md", "README_CN.md", "README_JA.md"]: 93 | replace_section(f, "", f"```\n{feat_str}\n```") 94 | 95 | script_path = joinPaths("source", "py", "in_browser.py") 96 | in_browser_script = read_text(script_path) 97 | rules = get_freeze_moving_rules() 98 | rules.sort() 99 | rule_arr_text = ( 100 | "[" + ", ".join([f'"{item}"' for item in rules]) + "]" 101 | ) 102 | patched = re.sub( 103 | r"MOVING_RULES = .*", 104 | f"MOVING_RULES = {rule_arr_text}", 105 | in_browser_script, 106 | ) 107 | write_text(script_path, patched) 108 | -------------------------------------------------------------------------------- /source/py/task/nerdfont.py: -------------------------------------------------------------------------------- 1 | import json 2 | from os import environ, path, remove 3 | from urllib.request import urlopen 4 | from fontTools.varLib import TTFont 5 | from fontTools.subset import Subsetter 6 | 7 | from source.py.utils import ( 8 | check_font_patcher, 9 | del_font_name, 10 | get_font_forge_bin, 11 | set_font_name, 12 | run, 13 | ) 14 | 15 | base_font_path = "fonts/TTF/MapleMono-Regular.ttf" 16 | family_name = "Maple Mono" 17 | font_forge_bin = get_font_forge_bin() 18 | 19 | if not path.exists(base_font_path): 20 | print("font not exist, please run this command first:\n\n python build.py --ttf-only --no-nerd-font --least-styles\n") 21 | exit(1) 22 | 23 | 24 | def parse_codes_from_json(data) -> list[int]: 25 | """ 26 | Load unicodes from `glyphnames.json` 27 | """ 28 | try: 29 | codes = [ 30 | int(f"0x{value['code']}", 16) 31 | for key, value in data.items() 32 | if isinstance(value, dict) and "code" in value 33 | ] 34 | 35 | return codes 36 | 37 | except json.JSONDecodeError: 38 | print("Invalide JSON") 39 | exit(1) 40 | 41 | 42 | def update_config_json(config_path: str, version: str): 43 | with open(config_path, "r+", encoding="utf-8") as file: 44 | data = json.load(file) 45 | 46 | if "nerd_font" in data: 47 | data["nerd_font"]["version"] = version 48 | 49 | file.seek(0) 50 | json.dump(data, file, ensure_ascii=False, indent=2) 51 | 52 | 53 | def check_update(): 54 | current_version = None 55 | with open("./config.json", "r", encoding="utf-8") as f: 56 | data = json.load(f) 57 | current_version = data["nerd_font"]["version"] 58 | 59 | latest_version = current_version 60 | print("Getting latest version from remote...") 61 | with urlopen( 62 | "https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest" 63 | ) as response: 64 | data = json.loads(response.read().decode("utf-8").split("\n")[0]) 65 | for key in data: 66 | if key == "tag_name": 67 | latest_version = str(data[key])[1:] 68 | break 69 | 70 | if latest_version == current_version: 71 | print("✨ Current version match latest version") 72 | if not check_font_patcher(latest_version): 73 | print("Font-Patcher not exist and fail to download, exit") 74 | exit(1) 75 | return 76 | 77 | print( 78 | f"Current version {current_version} not match latest version {latest_version}, update" 79 | ) 80 | if not check_font_patcher(latest_version, environ.get("GITHUB", "github.com")): 81 | print("Fail to update Font-Patcher, exit") 82 | exit(1) 83 | update_config_json("./config.json", latest_version) 84 | update_config_json("./source/preset-normal.json", latest_version) 85 | 86 | 87 | def get_nerd_font_patcher_args(mono: bool): 88 | # full args: https://github.com/ryanoasis/nerd-fonts?tab=readme-ov-file#font-patcher 89 | _nf_args = [ 90 | font_forge_bin, 91 | "FontPatcher/font-patcher", 92 | "-l", 93 | "-c", 94 | "--careful", 95 | ] 96 | if mono: 97 | _nf_args += ["--mono"] 98 | 99 | return _nf_args 100 | 101 | 102 | def build_nf(mono: bool): 103 | nf_args = get_nerd_font_patcher_args(mono) 104 | 105 | nf_file_name = "NerdFont" 106 | if mono: 107 | nf_file_name += "Mono" 108 | 109 | style_name = "Regular" 110 | 111 | run(nf_args + [base_font_path]) 112 | _path = f"{family_name.replace(' ', '')}{nf_file_name}-{style_name}.ttf" 113 | nf_font = TTFont(_path) 114 | remove(_path) 115 | 116 | set_font_name(nf_font, f"{family_name} NF Base{' Mono' if mono else ''}", 1) 117 | set_font_name(nf_font, style_name, 2) 118 | set_font_name( 119 | nf_font, f"{family_name} NF Base{' Mono' if mono else ''} {style_name}", 4 120 | ) 121 | set_font_name( 122 | nf_font, 123 | f"{family_name.replace(' ', '-')}-NF-Base{'-Mono' if mono else ''}-{style_name}", 124 | 6, 125 | ) 126 | del_font_name(nf_font, 16) 127 | del_font_name(nf_font, 17) 128 | 129 | return nf_font 130 | 131 | 132 | def subset(mono: bool, unicodes: list[int]): 133 | font = build_nf(mono) 134 | subsetter = Subsetter() 135 | subsetter.populate( 136 | unicodes=unicodes, 137 | ) 138 | subsetter.subset(font) 139 | 140 | _path = f"source/MapleMono-NF-Base{'-Mono' if mono else ''}.ttf" 141 | font.save(_path) 142 | run(f"ftcli fix monospace {_path}") 143 | font.close() 144 | 145 | 146 | def nerd_font(no_update: bool): 147 | if not no_update: 148 | check_update() 149 | 150 | with open("./FontPatcher/glyphnames.json", "r", encoding="utf-8") as f: 151 | unicodes = parse_codes_from_json(json.load(f)) 152 | subset(True, unicodes=unicodes) 153 | subset(False, unicodes=unicodes) 154 | -------------------------------------------------------------------------------- /source/py/task/page.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | import sys 5 | from source.py.feature import ( 6 | get_cv_cn_version_info, 7 | get_cv_italic_version_info, 8 | get_cv_version_info, 9 | get_ss_version_info, 10 | get_total_feat_ts, 11 | ) 12 | from source.py.task._utils import read_json, read_text, write_json, write_text 13 | from source.py.utils import joinPaths 14 | from python_minifier import minify 15 | 16 | 17 | def run_git_command(args: list, cwd=None, check=True): 18 | """Run a Git command and return output, handling errors""" 19 | try: 20 | result = subprocess.run( 21 | args, cwd=cwd, check=check, capture_output=True, text=True 22 | ) 23 | return result.stdout.strip() 24 | except subprocess.CalledProcessError as e: 25 | print( 26 | f"Error: Failed to execute {' '.join(args)} in {cwd or os.getcwd()}: {e.stderr}" 27 | ) 28 | sys.exit(1) 29 | 30 | 31 | def page(submodule_path: str, var_dir: str, commit: bool = False) -> None: 32 | # Switch to main branch 33 | abs_submodule_path = os.path.abspath(submodule_path) 34 | if commit: 35 | if not os.path.exists(abs_submodule_path): 36 | print( 37 | f"Error: Submodule {submodule_path} does not exist, please run `git submodule update --init` first" 38 | ) 39 | sys.exit(1) 40 | run_git_command(["git", "submodule", "update", "--remote"]) 41 | run_git_command(["git", "checkout", "main"], cwd=abs_submodule_path) 42 | run_git_command(["git", "pull"], cwd=abs_submodule_path) 43 | 44 | # Update landing page data 45 | feature_data_base = joinPaths(submodule_path, "data", "features") 46 | os.makedirs(feature_data_base, exist_ok=True) 47 | write_json(joinPaths(feature_data_base, "cv.json"), get_cv_version_info()) 48 | write_json(joinPaths(feature_data_base, "cn.json"), get_cv_cn_version_info()) 49 | write_json( 50 | joinPaths(feature_data_base, "italic.json"), get_cv_italic_version_info() 51 | ) 52 | write_json(joinPaths(feature_data_base, "ss.json"), get_ss_version_info()) 53 | write_text( 54 | joinPaths(feature_data_base, "features.ts"), 55 | get_total_feat_ts(), 56 | ) 57 | 58 | data = read_json("config.json") 59 | del data["$schema"] 60 | write_json(joinPaths(feature_data_base, "config.json"), data) 61 | 62 | data = read_text(joinPaths("source", "py", "in_browser.py")) 63 | write_text(joinPaths(submodule_path, "data", "script.py"), minify(data)) 64 | 65 | font_dir = joinPaths(submodule_path, "public", "fonts") 66 | os.system("python build.py --ttf-only --no-nerd-font --least-styles") 67 | os.system(f"ftcli converter ft2wf -f woff2 {var_dir}") 68 | shutil.rmtree(font_dir, ignore_errors=True) 69 | os.makedirs(font_dir, exist_ok=True) 70 | for filename in os.listdir(var_dir): 71 | if filename.endswith(".woff2"): 72 | os.rename( 73 | joinPaths(var_dir, filename), 74 | joinPaths(font_dir, filename.replace(".woff2", "-VF.woff2")), 75 | ) 76 | 77 | # Commit changes if specified 78 | if commit: 79 | # Add all changes 80 | run_git_command(["git", "add", "."], cwd=abs_submodule_path) 81 | 82 | # Commit changes 83 | run_git_command( 84 | ["git", "commit", "-m", "Update landing page data"], cwd=abs_submodule_path 85 | ) 86 | 87 | # Push to remote 88 | run_git_command(["git", "push", "origin", "main"], cwd=abs_submodule_path) 89 | 90 | # Reset to HEAD 91 | run_git_command(["git", "submodule", "update", "--remote"]) 92 | -------------------------------------------------------------------------------- /source/py/task/release.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | from typing import Callable 5 | from fontTools.ttLib import TTFont 6 | from source.py.task._utils import write_json, write_text 7 | from source.py.utils import joinPaths, run 8 | from build import main 9 | 10 | # Mapping of style names to weights 11 | weight_map = { 12 | "Thin": "100", 13 | "ExtraLight": "200", 14 | "Light": "300", 15 | "Regular": "400", 16 | "Italic": "400", 17 | "SemiBold": "500", 18 | "Medium": "600", 19 | "Bold": "700", 20 | "ExtraBold": "800", 21 | } 22 | 23 | 24 | def format_fontsource_name(filename: str): 25 | match = re.match(r"MapleMono-(.*)\.(.*)$", filename) 26 | 27 | if not match: 28 | return None 29 | 30 | style = match.group(1) 31 | 32 | weight = weight_map[style.removesuffix("Italic") if style != "Italic" else "Italic"] 33 | suf = "italic" if "italic" in filename.lower() else "normal" 34 | 35 | new_filename = f"maple-mono-latin-{weight}-{suf}.{match.group(2)}" 36 | return new_filename 37 | 38 | 39 | def format_woff2_name(filename: str): 40 | return filename.replace(".woff2", "-VF.woff2") 41 | 42 | 43 | def rename_woff_files(dir: str, fn: Callable[[str], str | None]): 44 | for filename in os.listdir(dir): 45 | if not filename.endswith(".woff") and not filename.endswith(".woff2"): 46 | continue 47 | new_name = fn(filename) 48 | if new_name: 49 | os.rename(joinPaths(dir, filename), joinPaths(dir, new_name)) 50 | print(f"Renamed: {filename} -> {new_name}") 51 | 52 | 53 | def parse_tag(type: str): 54 | out = os.popen(f"uv version --bump {type}").readline() 55 | return "v" + out.split(" ")[-1][:-1] 56 | 57 | 58 | def update_build_script_version(script_path: str, tag: str): 59 | with open(script_path, "r", encoding="utf-8", newline="\n") as f: 60 | content = re.sub(r'FONT_VERSION = ".*"', f'FONT_VERSION = "{tag}"', f.read()) 61 | write_text(script_path, content) 62 | 63 | 64 | def git_release_commit(tag, files): 65 | run(f"git add {' '.join(files)}") 66 | run(["git", "commit", "-m", f"Release {tag}"]) 67 | run(f"git tag {tag}") 68 | print("Committed and tagged") 69 | 70 | run("git push origin") 71 | run(f"git push origin {tag}") 72 | print("Pushed to origin") 73 | 74 | 75 | def format_font_map_key(key: int) -> str: 76 | formatted_key = f"{key:05X}" 77 | if formatted_key.startswith("0"): 78 | return formatted_key[1:] 79 | return formatted_key 80 | 81 | 82 | def write_unicode_map_json(font_path: str, output: str): 83 | font = TTFont(font_path) 84 | font_map = { 85 | format_font_map_key(k): v 86 | for k, v in font.getBestCmap().items() 87 | if k is not None 88 | } 89 | write_json(output, font_map) 90 | print(f"Write font map to {output}") 91 | font.close() 92 | 93 | 94 | def release(type: str, dry: bool): 95 | tag = parse_tag(type) 96 | # prompt and wait for user input 97 | choose = input(f"{'[DRY] ' if dry else ''}Tag {tag}? (Y or n) ") 98 | if choose != "" and choose.lower() != "y": 99 | print("Aborted") 100 | return 101 | 102 | script_path = "build.py" 103 | update_build_script_version(script_path, tag) 104 | target_fontsource_dir = "cdn/fontsource" 105 | main(["--ttf-only", "--no-nerd-font", "--cn", "--no-hinted"], tag) 106 | 107 | shutil.rmtree("./cdn", ignore_errors=True) 108 | run(f"ftcli converter ft2wf -f woff2 ./fonts/TTF -out {target_fontsource_dir}") 109 | run(f"ftcli converter ft2wf -f woff ./fonts/TTF -out {target_fontsource_dir}") 110 | rename_woff_files(target_fontsource_dir, format_fontsource_name) 111 | print("Generate fontsource files") 112 | 113 | dep_file = "requirements.txt" 114 | run( 115 | f"uv export --format requirements-txt --no-hashes --output-file {dep_file} --quiet" 116 | ) 117 | 118 | shutil.copytree("./fonts/CN", "./cdn/cn") 119 | print("Generate CN files") 120 | 121 | woff2_dir = "woff2/var" 122 | if os.path.exists(target_fontsource_dir): 123 | shutil.rmtree(woff2_dir) 124 | run(f"ftcli converter ft2wf -f woff2 ./fonts/Variable -out {woff2_dir}") 125 | rename_woff_files(woff2_dir, format_woff2_name) 126 | 127 | # write_unicode_map_json( 128 | # "./fonts/TTF/MapleMono-Regular.ttf", "./resources/glyph-map.json" 129 | # ) 130 | 131 | if dry: 132 | print("Dry run") 133 | else: 134 | git_release_commit(tag, [script_path, "woff2", dep_file, "pyproject.toml"]) 135 | -------------------------------------------------------------------------------- /source/py/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from os import environ, path, remove, walk 3 | import sys 4 | import shutil 5 | import subprocess 6 | from urllib.request import Request, urlopen 7 | from zipfile import ZIP_DEFLATED, ZipFile 8 | from fontTools.ttLib import TTFont 9 | from fontTools.merge import Merger 10 | from glyphsLib import GSFont 11 | 12 | 13 | def is_ci(): 14 | ci_envs = [ 15 | "JENKINS_HOME", 16 | "TRAVIS", 17 | "CIRCLECI", 18 | "GITHUB_ACTIONS", 19 | "GITLAB_CI", 20 | "TF_BUILD", 21 | ] 22 | 23 | for env in ci_envs: 24 | if environ.get(env): 25 | return True 26 | 27 | return False 28 | 29 | 30 | def run(command, extra_args=None, log=not is_ci()): 31 | """ 32 | Run a command line interface (CLI) command. 33 | """ 34 | if extra_args is None: 35 | extra_args = [] 36 | if isinstance(command, str): 37 | command = command.split() 38 | subprocess.run( 39 | command + extra_args, 40 | stdout=subprocess.DEVNULL if not log else None, 41 | check=True, 42 | ) 43 | 44 | 45 | def set_font_name(font: TTFont, name: str, id: int): 46 | font["name"].setName(name, nameID=id, platformID=1, platEncID=0, langID=0x0) # type: ignore 47 | font["name"].setName(name, nameID=id, platformID=3, platEncID=1, langID=0x409) # type: ignore 48 | 49 | 50 | def get_font_name(font: TTFont, id: int) -> str: 51 | return ( 52 | font["name"] 53 | .getName(nameID=id, platformID=3, platEncID=1, langID=0x409) # type: ignore 54 | .__str__() 55 | ) 56 | 57 | 58 | def del_font_name(font: TTFont, id: int): 59 | font["name"].removeNames(nameID=id) # type: ignore 60 | 61 | 62 | def joinPaths(*args: str) -> str: 63 | return "/".join(args) 64 | 65 | 66 | def is_windows(): 67 | return sys.platform == "win32" 68 | 69 | 70 | def is_macos(): 71 | return sys.platform == "darwin" 72 | 73 | 74 | def get_font_forge_bin(): 75 | WIN_FONTFORGE_PATH = "C:/Program Files (x86)/FontForgeBuilds/bin/fontforge.exe" 76 | MAC_FONTFORGE_PATH = ( 77 | "/Applications/FontForge.app/Contents/Resources/opt/local/bin/fontforge" 78 | ) 79 | LINUX_FONTFORGE_PATH = "/usr/bin/fontforge" 80 | 81 | result = "" 82 | if is_macos(): 83 | result = MAC_FONTFORGE_PATH 84 | elif is_windows(): 85 | result = WIN_FONTFORGE_PATH 86 | else: 87 | result = LINUX_FONTFORGE_PATH 88 | 89 | if not path.exists(result): 90 | result = shutil.which("fontforge") 91 | 92 | return result 93 | 94 | 95 | def parse_github_mirror(github_mirror: str) -> str: 96 | github = environ.get("GITHUB") # custom github mirror, for CN users 97 | if not github: 98 | github = github_mirror 99 | return f"https://{github}" 100 | 101 | 102 | def download_file(url: str, target_path: str): 103 | user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" 104 | req = Request(url, headers={"User-Agent": user_agent}) 105 | not_ci = not is_ci() 106 | with urlopen(req) as response, open(target_path, "wb") as out_file: 107 | total_size = int(response.getheader("Content-Length").strip()) 108 | downloaded_size = 0 109 | block_size = 8192 110 | 111 | while True: 112 | buffer = response.read(block_size) 113 | if not buffer: 114 | break 115 | 116 | out_file.write(buffer) 117 | 118 | if not_ci: 119 | downloaded_size += len(buffer) 120 | percent_downloaded = (downloaded_size / total_size) * 100 121 | print( 122 | f"Downloading: [{percent_downloaded:.2f}%] {downloaded_size} / {total_size}", 123 | end="\r", 124 | ) 125 | 126 | 127 | def download_zip_and_extract( 128 | name: str, url: str, zip_path: str, output_dir: str, remove_zip: bool = False 129 | ) -> bool: 130 | if not path.exists(zip_path): 131 | print(f"{name} does not exist, download from {url}") 132 | try: 133 | download_file(url, target_path=zip_path) 134 | except Exception as e: 135 | print( 136 | f"❗\nFail to download {name}. Please check your internet connection or download it manually from {url}, then put downloaded zip into project's root and run this script again. \n Error: {e}" 137 | ) 138 | return False 139 | try: 140 | with ZipFile(zip_path, "r") as zip_ref: 141 | zip_ref.extractall(output_dir) 142 | if remove_zip: 143 | remove(zip_path) 144 | return True 145 | except Exception as e: 146 | print(f"❗Fail to extract {name}. Error: {e}") 147 | return False 148 | 149 | 150 | def check_font_patcher( 151 | version: str, github_mirror: str = "github.com", target_dir: str = "FontPatcher" 152 | ) -> bool: 153 | bin_path = f"{target_dir}/font-patcher" 154 | if path.exists(target_dir): 155 | with open(bin_path, "r", encoding="utf-8") as f: 156 | if f"# Nerd Fonts Version: {version}" in f.read(): 157 | return True 158 | else: 159 | print("FontPatcher version not match, delete it") 160 | shutil.rmtree("FontPatcher", ignore_errors=True) 161 | 162 | zip_path = "FontPatcher.zip" 163 | url = f"https://{github_mirror}/ryanoasis/nerd-fonts/releases/download/v{version}/{zip_path}" 164 | if not download_zip_and_extract( 165 | name="Nerd Font Patcher", url=url, zip_path=zip_path, output_dir=target_dir 166 | ): 167 | return False 168 | 169 | with open(bin_path, "r", encoding="utf-8") as f: 170 | if f"# Nerd Fonts Version: {version}" in f.read(): 171 | return True 172 | 173 | print(f"❗FontPatcher version is not {version}, please download it from {url}") 174 | return False 175 | 176 | 177 | def download_cn_base_font( 178 | tag: str, zip_path: str, target_dir: str, github_mirror: str = "github.com" 179 | ) -> bool: 180 | url = f"https://{github_mirror}/subframe7536/maple-font/releases/download/{tag}/{zip_path}" 181 | return download_zip_and_extract( 182 | name=f"{'Static' if 'static' in zip_path else 'Variable'} CN Base Font", 183 | url=url, 184 | zip_path=zip_path, 185 | output_dir=target_dir, 186 | ) 187 | 188 | 189 | def match_unicode_names(file_path: str) -> dict[str, str]: 190 | font = GSFont(file_path) 191 | result = {} 192 | 193 | for glyph in font.glyphs: 194 | glyph_name = glyph.name 195 | unicode_values = glyph.unicode 196 | 197 | if glyph_name and unicode_values: 198 | unicode_str = f"uni{''.join(unicode_values).upper().zfill(4)}" 199 | result[unicode_str] = glyph_name 200 | 201 | return result 202 | 203 | 204 | # https://github.com/subframe7536/maple-font/issues/314 205 | def verify_glyph_width( 206 | font: TTFont, expect_widths: list[int], file_name: str | None = None 207 | ): 208 | print("Verify glyph width") 209 | result = [] 210 | for name in font.getGlyphNames(): 211 | width, _ = font["hmtx"][name] # type: ignore 212 | if width not in expect_widths: 213 | result.append([name, width]) 214 | 215 | if result.__len__() > 0: 216 | print(f"Every glyph's width should be in {expect_widths}, but these are not:") 217 | for item in result: 218 | print(f"{item[0]} => {item[1]}") 219 | 220 | raise Exception( 221 | f"{file_name or 'The font'} may contain glyphs that width is not in {expect_widths}, which may broke monospace rule." 222 | ) 223 | 224 | 225 | def compress_folder( 226 | source_file_or_dir_path: str, 227 | target_parent_dir_path: str, 228 | family_name_compact: str, 229 | suffix: str, 230 | build_config_path: str, 231 | ) -> tuple[str, str]: 232 | """ 233 | Archive folder and return sha1 and file name 234 | """ 235 | source_folder_name = path.basename(source_file_or_dir_path) 236 | 237 | zip_name_without_ext = f"{family_name_compact}-{source_folder_name}{suffix}" 238 | 239 | zip_path = joinPaths( 240 | target_parent_dir_path, 241 | f"{zip_name_without_ext}.zip", 242 | ) 243 | 244 | with ZipFile(zip_path, "w", compression=ZIP_DEFLATED, compresslevel=5) as zip_file: 245 | for root, _, files in walk(source_file_or_dir_path): 246 | for file in files: 247 | file_path = joinPaths(root, file) 248 | zip_file.write( 249 | file_path, path.relpath(file_path, source_file_or_dir_path) 250 | ) 251 | zip_file.write("OFL.txt", "LICENSE.txt") 252 | if not source_file_or_dir_path.endswith("Variable"): 253 | zip_file.write( 254 | build_config_path, 255 | "config.json", 256 | ) 257 | 258 | zip_file.close() 259 | sha256 = hashlib.sha256() 260 | with open(zip_path, "rb") as zip_file: 261 | while True: 262 | data = zip_file.read(1024) 263 | if not data: 264 | break 265 | sha256.update(data) 266 | 267 | return sha256.hexdigest(), zip_name_without_ext 268 | 269 | 270 | def get_directory_hash(dir_path: str) -> str: 271 | hasher = hashlib.sha256() 272 | for root, _, files in sorted(walk(dir_path)): 273 | for file in sorted(files): 274 | file_path = path.join(root, file) 275 | try: 276 | with open(file_path, "rb") as f: 277 | while True: 278 | # 4KB chunk size 279 | chunk = f.read(4096) 280 | if not chunk: 281 | break 282 | hasher.update(chunk) 283 | 284 | except (IOError, OSError) as e: 285 | raise Exception(f"Error reading file: {file_path} - {e}") 286 | 287 | return hasher.hexdigest() 288 | 289 | 290 | def check_directory_hash(dir_path: str) -> bool: 291 | if not path.exists(dir_path): 292 | print(f"{dir_path} not exist, skip computing hash") 293 | return False 294 | with open(f"{dir_path}.sha256", "r") as f: 295 | return f.readline() == get_directory_hash(dir_path) 296 | 297 | 298 | def merge_ttfonts( 299 | base_font_path: str, extra_font_path: str, use_pyftmerge: bool = False 300 | ) -> TTFont: 301 | """ 302 | Merge glyphs from ``source_font`` into ``base_font``, skipping duplicate glyph names. 303 | 304 | ``fontTools.merge.Merger`` will erase the glyph names, so merge them manually 305 | 306 | Args: 307 | base_font (str): The base font path to merge into 308 | source_font (str): The font path to merge from 309 | use_pyftmerge (bool): Force to use pyftmerge 310 | 311 | Returns: 312 | TTFont: The modified base_font with merged glyphs 313 | """ 314 | if use_pyftmerge: 315 | merger = Merger() 316 | return merger.merge([base_font_path, extra_font_path]) 317 | 318 | try: 319 | base_font = TTFont(base_font_path) 320 | extra_font = TTFont(extra_font_path) 321 | # Get glyph tables and orders 322 | base_glyf = base_font["glyf"] 323 | extra_glyf = extra_font["glyf"] 324 | base_glyph_order = base_font.getGlyphOrder() 325 | extra_glyph_order = extra_font.getGlyphOrder() 326 | 327 | base_hmtx = base_font["hmtx"] if "hmtx" in base_font else None 328 | extra_hmtx = extra_font["hmtx"] if "hmtx" in extra_font else None 329 | 330 | base_glyph_names = set(base_glyph_order) 331 | 332 | glyphs_to_add = [] 333 | 334 | for glyph_name in extra_glyph_order: 335 | if glyph_name not in base_glyph_names: 336 | # Copy glyph from source 337 | base_glyf.glyphs[glyph_name] = extra_glyf.glyphs[glyph_name] # type: ignore 338 | 339 | # Copy metrics if hmtx tables exist 340 | if base_hmtx and extra_hmtx and glyph_name in extra_hmtx.metrics: # type: ignore 341 | base_hmtx.metrics[glyph_name] = extra_hmtx.metrics[glyph_name] # type: ignore 342 | elif base_hmtx: 343 | # Fallback: use default metrics if source doesn't have them 344 | base_hmtx.metrics[glyph_name] = (0, 0) # type: ignore # advanceWidth, lsb 345 | 346 | glyphs_to_add.append(glyph_name) 347 | 348 | if not glyphs_to_add: 349 | print("No new glyphs to merge") 350 | return base_font 351 | 352 | # Update glyph order 353 | updated_glyph_order = base_glyph_order + glyphs_to_add 354 | base_font.setGlyphOrder(updated_glyph_order) 355 | 356 | # Update maxp table 357 | base_font["maxp"].numGlyphs = len(updated_glyph_order) # type: ignore 358 | 359 | # Update cmap if it exists 360 | if "cmap" in extra_font and "cmap" in base_font: 361 | base_cmap = base_font["cmap"].getBestCmap() # type: ignore 362 | extra_cmap = extra_font["cmap"].getBestCmap() # type: ignore 363 | if base_cmap and extra_cmap: 364 | for code, name in extra_cmap.items(): 365 | if name in glyphs_to_add and code not in base_cmap: 366 | base_cmap[code] = name 367 | 368 | # Update hhea table if it exists 369 | if "hhea" in base_font: 370 | if base_hmtx: 371 | # Ensure hhea matches the number of hmtx entries 372 | base_font["hhea"].numberOfHMetrics = len(base_hmtx.metrics) # type: ignore 373 | base_font["hhea"].recalc(base_font) # type: ignore 374 | 375 | return base_font 376 | 377 | except Exception as e: 378 | print(f"Error merging fonts: {str(e)}") 379 | raise 380 | -------------------------------------------------------------------------------- /task.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | from os import path 4 | import shutil 5 | 6 | 7 | def main(): 8 | parser = argparse.ArgumentParser(description="Task script for Maple Font") 9 | 10 | command = parser.add_subparsers(dest="command", help="Total tasks") 11 | 12 | nerdfont_parser = command.add_parser("nerd-font", help="Build Nerd-Font base font") 13 | nerdfont_parser.add_argument( 14 | "--no-update", 15 | action="store_true", 16 | help="Do not check version and update if available", 17 | ) 18 | 19 | feature_parser = command.add_parser("fea", help="Build fea files") 20 | feature_parser.add_argument( 21 | "--output", type=str, default="./source/features", help="Output directory" 22 | ) 23 | feature_parser.add_argument( 24 | "--cn", 25 | action="store_true", 26 | help="Generate features that contains CN features, remove exists CN feature files if not set", 27 | ) 28 | 29 | release_parser = command.add_parser("release", help="Release new version") 30 | release_parser.add_argument( 31 | "type", 32 | choices=["major", "minor"], 33 | help="Bump version type", 34 | ) 35 | release_parser.add_argument( 36 | "--dry", 37 | action="store_true", 38 | help="Dry run", 39 | ) 40 | 41 | page_parser = command.add_parser("page", help="Update landing page data") 42 | page_parser.add_argument("--commit", action="store_true", help="Commit changes") 43 | 44 | args = parser.parse_args() 45 | if args.command == "nerd-font": 46 | from source.py.task.nerdfont import nerd_font 47 | 48 | nerd_font(args.no_update) 49 | 50 | elif args.command == "fea": 51 | from source.py.task.fea import fea 52 | 53 | fea(args.output, args.cn) 54 | 55 | elif args.command == "release": 56 | from source.py.task.release import release 57 | 58 | release(args.type, args.dry) 59 | elif args.command == "page": 60 | from source.py.task.page import page 61 | 62 | page("./maple-font-page", "./fonts/Variable", args.commit) 63 | else: 64 | print("Test only") 65 | from source.py.in_browser import main 66 | 67 | zip_path = "./fonts/archive/MapleMono-NF-CN-unhinted.zip" 68 | if not path.exists(zip_path): 69 | print("No zip file, please run `uv run build --archive` first") 70 | return 71 | test_path = zip_path.replace(".zip", "-test.zip") 72 | shutil.copy(zip_path, test_path) 73 | main( 74 | test_path, 75 | zip_path.replace(".zip", "-result.zip"), 76 | {"cv01": "1", "cv02": "1"}, 77 | ) 78 | 79 | 80 | if __name__ == "__main__": 81 | main() 82 | -------------------------------------------------------------------------------- /woff2/var/MapleMono-Italic[wght]-VF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subframe7536/maple-font/2fb41712b3dd8231d1fffe48ee70c0bfb8ce2ffe/woff2/var/MapleMono-Italic[wght]-VF.woff2 -------------------------------------------------------------------------------- /woff2/var/MapleMono[wght]-VF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subframe7536/maple-font/2fb41712b3dd8231d1fffe48ee70c0bfb8ce2ffe/woff2/var/MapleMono[wght]-VF.woff2 --------------------------------------------------------------------------------