%% cmgraded.sty
%%
%% Computer Modern Graded Fonts / LaTeX Package 'cmgraded'
%%
%%   Classic LaTeX Look and Feel with LuaLaTeX and OpenType
%%          Fonts in different Grades of Blackness
%%
%% This LaTeX package with name "cmgraded" contains OpenType and Type 1
%% (Adobe/PostScript) fonts named "Computer Modern Graded" that have been
%% generated from the original "Computer Modern" fonts by Donald E. Knuth
%% and TeX metrics by applying different grades of "blackness" via METAFONT
%% that make the fonts gradually darker, while keeping metrics intact and
%% not turning a regular weight font visually into a bold one.
%%
%% Copyright (c) 2026 Alain Stalder (alain@exactphilosophy.net).
%% https://www.exactphilosophy.net/patafont
%%
%% This work may be distributed and/or modified under the conditions of the
%% LaTeX Project Public License, either version 1.3c of this license or (at
%% your option) any later version. The latest version of this license is in
%% https://www.latex-project.org/lppl.txt and version 1.3c or later is part
%% of all distributions of LaTeX version 2008 or later.
%%
%% This work has the LPPL maintenance status 'maintained'.
%% Current Maintainer of this work is Alain Stalder (alain@exactphilosophy.net).
%%
%% The fonts in this package are licensed separately under the SIL Open Font
%% License v1.1, with Reserved Font Name "Computer Modern Graded", see the
%% OFL.txt included with this package and note that the OpenType fonts also
%% contain the full license inside in their name tables.
%%
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{cmgraded}[2026/07/29 v1.0.0 Grade-applied METAFONT Computer Modern as OpenType and Type 1]
\RequirePackage{iftex}
\RequirePackage{kvoptions}
\SetupKeyvalOptions{family=cmg,prefix=cmg@}
\DeclareStringOption[19]{grade}
\DeclareStringOption[]{text}%       shorthand for serif+sans+typewriter
\DeclareStringOption[]{serif}%      auto|type1|opentype|none
\DeclareStringOption[]{sans}%
\DeclareStringOption[]{typewriter}%
\DeclareStringOption[]{math}%
\DeclareBoolOption[false]{allowunsupported}%
\DeclareStringOption[classic]{italiccorrection}% classic|modern: refine the OpenType-text italic correction (lualatex)
\DeclareStringOption[modern]{hyphenation}% accentsclassic|modern: OT1-style accent vs full LuaTeX hyphenation (lualatex)
\DeclareStringOption[classic]{ttsentencespacing}% classic|modern: two spaces after a period in \texttt (classic cmtt EXTRASPACE) vs lualatex's single (lualatex OpenType)
\DeclareStringOption[modern]{quotedsentencespacing}% classic|modern (GLOBAL): pdflatex-OT1's narrower vs lualatex's sentence space after '.<closing quote>'
\ProcessKeyvalOptions*

\def\cmg@auto{auto}\def\cmg@typeone{type1}\def\cmg@opentype{opentype}\def\cmg@none{none}\def\cmg@empty{}
\def\cmg@err#1{\PackageError{cmgraded}{#1}{See the package documentation.}}
\def\cmg@info#1{\PackageInfo{cmgraded}{#1}}

%% italiccorrection (classic|modern) and hyphenation (accentsclassic|modern):
%% 'classic'/'accentsclassic' reproduce OT1, 'modern' uses the full LuaTeX
%% behaviour. Hyphenation is 'accentsclassic' rather than 'classic' because it
%% reproduces OT1 only for *accents* (hyphenating an accented word up to its first
%% accent); other words follow LuaTeX's own patterns, which differ from pdflatex's
%% below this package. Both refine the OpenType-text path under lualatex; under
%% other engines the classic behaviour is inherent, so the keys are a quiet no-op.
\def\cmg@classic{classic}\def\cmg@modern{modern}\def\cmg@accentsclassic{accentsclassic}
\newif\ifcmg@italicclassic
\newif\ifcmg@hyphenclassic
\newif\ifcmg@ttspaceclassic
\newif\ifcmg@quotedclassic
\ifx\cmg@italiccorrection\cmg@classic \cmg@italicclassictrue
\else\ifx\cmg@italiccorrection\cmg@modern \cmg@italicclassicfalse
\else \cmg@err{italiccorrection must be `classic' or `modern' (got `\cmg@italiccorrection')}\cmg@italicclassictrue\fi\fi
\ifx\cmg@hyphenation\cmg@accentsclassic \cmg@hyphenclassictrue
\else\ifx\cmg@hyphenation\cmg@modern \cmg@hyphenclassicfalse
\else \cmg@err{hyphenation must be `accentsclassic' or `modern' (got `\cmg@hyphenation')}\cmg@hyphenclassicfalse\fi\fi
\ifx\cmg@ttsentencespacing\cmg@classic \cmg@ttspaceclassictrue
\else\ifx\cmg@ttsentencespacing\cmg@modern \cmg@ttspaceclassicfalse
\else \cmg@err{ttsentencespacing must be `classic' or `modern' (got `\cmg@ttsentencespacing')}\cmg@ttspaceclassictrue\fi\fi
\ifx\cmg@quotedsentencespacing\cmg@classic \cmg@quotedclassictrue
\else\ifx\cmg@quotedsentencespacing\cmg@modern \cmg@quotedclassicfalse
\else \cmg@err{quotedsentencespacing must be `classic' or `modern' (got `\cmg@quotedsentencespacing')}\cmg@quotedclassicfalse\fi\fi

%% Missing-grade signpost: only some grades are installed (the package ships the
%% seven presets a..g); a numeric grade whose fonts are absent gets a pointer to
%% the full set rather than a cryptic missing-font failure. Shown at most once.
\newif\ifcmg@gradeshown
\def\cmg@gradegone{\ifcmg@gradeshown\else\cmg@gradeshowntrue
  \cmg@err{grade \cmg@grade\space is not installed. This package ships the seven presets a..g (grades 0, 10, 19, 32, 44, 57, 72); the full range of grades 0..72 for finer control is available from https://www.exactphilosophy.net/patafont}\fi}

%% Grade letters a..g (case-insensitive): memorable aliases for the seven
%% perceptually-even grades (a=lightest ... g=heaviest). Numeric grades 0..72
%% still work for fine control. Mapping: a=0 b=10 c=19 d=32 e=44 f=57 g=72.
\newif\ifcmg@gletter
\newif\ifcmg@gok
\def\cmg@ckdigits#1{% set \cmg@gokfalse if any token of the value is not a digit
  \ifx\relax#1\else
    \ifnum`#1>`/ \ifnum`#1<`: \else\cmg@gokfalse\fi\else\cmg@gokfalse\fi
    \expandafter\cmg@ckdigits
  \fi}
\def\cmg@gradealias#1#2{\def\cmg@tmp{#1}\ifx\cmg@grade\cmg@tmp\def\cmg@grade{#2}\cmg@glettertrue\fi}
\cmg@gradealias{a}{0}\cmg@gradealias{A}{0}%
\cmg@gradealias{b}{10}\cmg@gradealias{B}{10}%
\cmg@gradealias{c}{19}\cmg@gradealias{C}{19}%
\cmg@gradealias{d}{32}\cmg@gradealias{D}{32}%
\cmg@gradealias{e}{44}\cmg@gradealias{E}{44}%
\cmg@gradealias{f}{57}\cmg@gradealias{F}{57}%
\cmg@gradealias{g}{72}\cmg@gradealias{G}{72}%
\ifcmg@gletter\else
  \cmg@goktrue
  \expandafter\cmg@ckdigits\cmg@grade\relax
  \ifcmg@gok\else
    \cmg@err{grade `\cmg@grade' is invalid; use an integer 0..72 or a letter a..g}%
    \def\cmg@grade{19}%
  \fi
\fi

%% grade range 0..72
\ifnum\cmg@grade<0 \cmg@err{grade \cmg@grade\space out of range 0..72}\fi
\ifnum\cmg@grade>72 \cmg@err{grade \cmg@grade\space out of range 0..72}\fi

%% italiccorrection/hyphenation act only on the OpenType-text path under lualatex
%% (both are luatex mechanisms). Under other engines they are a quiet no-op -- the
%% classic behaviour is inherent there -- so there is nothing to check here.

%% quotedsentencespacing=classic (opt-in, GLOBAL): give the closing double quote
%% U+201D a space factor of 1000, so a sentence-ending '.'/':' just before it does
%% not carry its extra (non-French) space into the following gap -- matching
%% pdflatex+OT1, where the closing quote resets the factor. Default 'modern' keeps
%% lualatex's (sentence) space. GLOBAL because \sfcode is keyed by character code,
%% not font, so it affects '."' spacing for every font in the document. Only the
%% Unicode (OpenType) engines address U+201D directly; pdftex is 8-bit and would
%% reject the code point, so it is skipped there (and OT1 already matches). Applied
%% at \begin{document} so it wins over babel/csquotes etc.
\ifcmg@quotedclassic
  \ifPDFTeX\else
    \AtBeginDocument{\sfcode"201D=1000\relax}%
  \fi
\fi

%% NFSS Type 1 families, the map, and the OpenType (fontspec) family names.
\edef\cmg@t@r {cmgg\cmg@grade r}%
\edef\cmg@t@ss{cmgg\cmg@grade ss}%
\edef\cmg@t@tt{cmgg\cmg@grade tt}%
\edef\cmg@t@m {cmgg\cmg@grade m}%
\edef\cmg@t@sy{cmgg\cmg@grade sy}%
\edef\cmg@t@ex{cmgg\cmg@grade ex}%
\edef\cmg@map {cmgg\cmg@grade.map}%
\edef\cmg@o@serif{Computer Modern Graded G\cmg@grade\space Serif}%
\edef\cmg@o@sans {Computer Modern Graded G\cmg@grade\space Sans}%
\edef\cmg@o@tt   {Computer Modern Graded G\cmg@grade\space Typewriter}%

%% Engine 'auto' formats (text and math).
\ifLuaTeX
  \def\cmg@textauto{opentype}\def\cmg@mathauto{type1}%
\else\ifXeTeX
  \def\cmg@textauto{opentype}\def\cmg@mathauto{type1}%
\else\ifPDFTeX
  \def\cmg@textauto{type1}\def\cmg@mathauto{type1}%
\else
  \cmg@err{unsupported engine (need pdflatex, lualatex, or xelatex)}%
  \def\cmg@textauto{type1}\def\cmg@mathauto{type1}%
\fi\fi\fi

%% Effective per-face value: longhand overrides the 'text' shorthand, else auto.
\def\cmg@effface#1#2{%
  \ifx#2\cmg@empty
    \ifx\cmg@text\cmg@empty \let#1\cmg@auto \else \let#1\cmg@text \fi
  \else \let#1#2\fi}
\cmg@effface\cmg@serifF\cmg@serif
\cmg@effface\cmg@sansF\cmg@sans
\cmg@effface\cmg@ttF\cmg@typewriter
\ifx\cmg@math\cmg@empty \let\cmg@mathF\cmg@auto \else \let\cmg@mathF\cmg@math \fi

%% The three text faces share ONE format: collect explicit type1/opentype
%% signals; more than one distinct -> error; else the engine 'auto' text format.
\let\cmg@fmt\relax
\def\cmg@recordfmt#1{%
  \ifx\cmg@fmt\relax \let\cmg@fmt#1\else
    \ifx\cmg@fmt#1\else
      \cmg@err{all text faces must share one format (got a mix of type1 and opentype)}%
    \fi\fi}
\def\cmg@seefmt#1{%
  \ifx#1\cmg@typeone \cmg@recordfmt\cmg@typeone \else
    \ifx#1\cmg@opentype \cmg@recordfmt\cmg@opentype \fi\fi}
\cmg@seefmt\cmg@serifF \cmg@seefmt\cmg@sansF \cmg@seefmt\cmg@ttF
\ifx\cmg@fmt\relax \let\cmg@fmt\cmg@textauto \fi
\ifx\cmg@mathF\cmg@auto \let\cmg@mathF\cmg@mathauto \fi

%% Presence (on unless 'none') and derived flags.
\newif\ifcmg@serifon \newif\ifcmg@sanson \newif\ifcmg@tton
\ifx\cmg@serifF\cmg@none \cmg@serifonfalse \else \cmg@serifontrue \fi
\ifx\cmg@sansF \cmg@none \cmg@sansonfalse  \else \cmg@sansontrue  \fi
\ifx\cmg@ttF   \cmg@none \cmg@ttonfalse    \else \cmg@ttontrue    \fi
\newif\ifcmg@anytext
\ifcmg@serifon\cmg@anytexttrue\fi \ifcmg@sanson\cmg@anytexttrue\fi \ifcmg@tton\cmg@anytexttrue\fi
\newif\ifcmg@texttypeone \newif\ifcmg@textopentype \newif\ifcmg@mathtypeone
\ifcmg@anytext
  \ifx\cmg@fmt\cmg@typeone \cmg@texttypeonetrue\fi
  \ifx\cmg@fmt\cmg@opentype \cmg@textopentypetrue\fi
\fi
\ifx\cmg@mathF\cmg@typeone \cmg@mathtypeonetrue\fi

%% Support policy: unless 'allowunsupported', restrict to the maintained combo
%% (lualatex + OpenType text + Type 1 math). Checked here at load, where the
%% engine and the resolved text format are both known. One \cmg@unsupported message,
%% parameterised by engine/format/detail; only one case can apply (engine-disjoint).
\def\cmg@unsupported#1#2#3{%
  \cmg@err{To use the #1 engine (incl. for using #2)#3, you must set the package key 'allowunsupported', e.g. '\string\usepackage[allowunsupported]\string{cmgraded\string}', but note that #1#3 is only technically supported in a basic way with no plans to ever fully support it; the focus of this package is on OpenType fonts in lualatex to quite faithfully reproduce the output of the original latex including especially grading/blackening for the Computer Modern fonts with OT1 font encoding using METAFONT mechanisms, typically ignoring typographical changes after Knuth's freeze around 1992; see the package documentation for more details and note that other packages would be free to use the fonts in this package and add features into other directions}%
}
\unless\ifcmg@allowunsupported
  \ifPDFTeX
    \cmg@unsupported{pdftex}{pdflatex}{}%
  \else\ifXeTeX
    \cmg@unsupported{xetex}{xelatex}{}%
  \else\ifLuaTeX
    \ifcmg@texttypeone
      \cmg@unsupported{luatex}{lualatex}{\space with Type 1 (Adobe/PostScript) fonts}%
    \fi
  \fi\fi\fi
\fi

%% Guardrails (capability-based; only pdflatex+OpenType is a hard 'no').
\ifx\cmg@mathF\cmg@opentype \cmg@err{OpenType math is not yet available (planned as a later phase); use math=type1 or math=none}\fi
\ifPDFTeX \ifcmg@textopentype \cmg@err{pdflatex cannot embed OpenType; use text=type1}\fi \fi

%% Register the map whenever any Type 1 side is active.
\newif\ifcmg@anytypeone
\ifcmg@texttypeone\cmg@anytypeonetrue\fi \ifcmg@mathtypeone\cmg@anytypeonetrue\fi
\ifcmg@anytypeone
  \IfFileExists{ot1cmgg\cmg@grade r.fd}{}{\cmg@gradegone}% grade's Type 1 fonts present?
  \ifLuaTeX \pdfextension mapfile{+\cmg@map}%
  \else\ifPDFTeX \pdfmapfile{+\cmg@map}%
  \else\ifXeTeX \AtBeginDocument{\special{dvipdfmx:config f \cmg@map}}%
  \fi\fi\fi
\fi

%% --- text ---
\ifcmg@textopentype
  \RequirePackage{fontspec}%
  %% The classic refinements below are luatex mechanisms; skip them under xetex
  %% (which has no \directlua), where their absence is harmless.
  \ifLuaTeX
    %% italiccorrection=classic: register the luaotfload patch BEFORE the fonts
    %% load, so every CMG oblique face gets the exact METAFONT charic (else
    %% luaotfload's ink-based, grade-drifting correction is used).
    \ifcmg@italicclassic
      \directlua{local f=kpse.find_file("cmgraded-italiccorrection.lua") if f then dofile(f) end}%
    \fi
    %% hyphenation=accentsclassic: reproduce OT1+pdflatex, where TeX's \accent
    %% makes an accented letter a boundary -- only the run before a word's first
    %% accent is hyphenated automatically (the precomposed OpenType glyphs would
    %% otherwise hyphenate in full, the modern LuaTeX way).
    \ifcmg@hyphenclassic
      \directlua{local f=kpse.find_file("cmgraded-hyphenation.lua") if f then dofile(f) end}%
      \cmg@info{hyphenation=accentsclassic: accented words are hyphenated only up to
        their first accent, as in OT1+pdflatex (mark the rest by hand with
        \string\-); use hyphenation=modern for full (default LuaTeX) hyphenation}%
    \fi
    %% ttsentencespacing=classic: luaotfload zeroes the extra sentence space
    %% (fontdimen7) for monospaced fonts, so lualatex puts ONE space after a
    %% period in \texttt where classic cmtt gives two. Restore it -- EXTRASPACE
    %% = a full space, as in cmtt10.tfm -- whenever the typewriter font is
    %% selected (per size), so \nonfrenchspacing yields the classic two spaces.
    \ifcmg@tton\ifcmg@ttspaceclassic
      \AddToHook{selectfont}{\ifx\f@family\ttdefault \fontdimen7\font=\fontdimen2\font\relax \fi}%
    \fi\fi
  \fi
  \ifcmg@serifon \begingroup\edef\x{\endgroup\noexpand\setmainfont{\cmg@o@serif}}\x\fi
  \ifcmg@sanson  \begingroup\edef\x{\endgroup\noexpand\setsansfont{\cmg@o@sans}}\x\fi
  \ifcmg@tton    \begingroup\edef\x{\endgroup\noexpand\setmonofont{\cmg@o@tt}}\x\fi
\fi
\ifcmg@texttypeone
  \AtBeginDocument{%
    \def\cmg@T@one{T1}%
    \ifx\encodingdefault\cmg@T@one
      \cmg@err{Type 1 Computer Modern Graded text needs OT1; remove \string\usepackage[T1]{fontenc}}%
    \fi
    \renewcommand\encodingdefault{OT1}%
    \ifcmg@serifon \let\rmdefault\cmg@t@r \fi
    \ifcmg@sanson  \let\sfdefault\cmg@t@ss \fi
    \ifcmg@tton    \let\ttdefault\cmg@t@tt \fi
    \normalfont
  }%
\fi

%% Force our begindocument code to run AFTER fontspec's, so fontspec has already
%% set up its private legacy-maths font (digits/+/=/()) before we re-point it
%% to the blackened cmr below. fontspec renamed that symbol font across versions
%% ('typeonemaths' -> 'legacymaths'), so we re-point whichever one is present.
%% \SetSymbolFont/\SetMathAlphabet are preamble-only, so
%% this must stay in the begindocument hook (not begindocument/end, where they are
%% already disabled) -- the ordering is enforced by rule, not by hook choice.
%% Without the rule the order is fragile: e.g. quotedsentencespacing=classic registers an earlier
%% \AtBeginDocument, which made this block run BEFORE fontspec, so symtypeonemaths was
%% not yet defined and the \@ifundefined guard below silently skipped the override,
%% leaking stock cmr into math. fontspec's begindocument label is engine-specific
%% (fontspec-luatex / fontspec-xetex); both rules are inert if that label is absent.
\DeclareHookRule{begindocument}{fontspec-luatex}{before}{cmgraded}%
\DeclareHookRule{begindocument}{fontspec-xetex}{before}{cmgraded}%
%% --- math (Type 1) --- re-declares the math symbol fonts at begin-document,
%% overriding fontspec's begindocument math re-declaration.
\ifcmg@mathtypeone
  \AtBeginDocument{%
    \begingroup\edef\x{\endgroup
      \noexpand\SetSymbolFont{operators}{normal}{OT1}{\cmg@t@r}{m}{n}%
      \noexpand\SetSymbolFont{operators}{bold}{OT1}{\cmg@t@r}{bx}{n}%
      \noexpand\SetSymbolFont{letters}{normal}{OML}{\cmg@t@m}{m}{it}%
      \noexpand\SetSymbolFont{letters}{bold}{OML}{\cmg@t@m}{b}{it}%
      \noexpand\SetSymbolFont{symbols}{normal}{OMS}{\cmg@t@sy}{m}{n}%
      \noexpand\SetSymbolFont{symbols}{bold}{OMS}{\cmg@t@sy}{b}{n}%
      \noexpand\SetSymbolFont{largesymbols}{normal}{OMX}{\cmg@t@ex}{m}{n}%
    }\x
    \@ifundefined{symtypeonemaths}{}{%
      \begingroup\edef\x{\endgroup
        \noexpand\SetSymbolFont{typeonemaths}{normal}{OT1}{\cmg@t@r}{m}{n}%
        \noexpand\SetSymbolFont{typeonemaths}{bold}{OT1}{\cmg@t@r}{bx}{n}%
      }\x
    }%
    \@ifundefined{symlegacymaths}{}{%
      \begingroup\edef\x{\endgroup
        \noexpand\SetSymbolFont{legacymaths}{normal}{OT1}{\cmg@t@r}{m}{n}%
        \noexpand\SetSymbolFont{legacymaths}{bold}{OT1}{\cmg@t@r}{bx}{n}%
      }\x
    }%
    \begingroup\edef\x{\endgroup
      \noexpand\SetMathAlphabet{\noexpand\mathrm}{normal}{OT1}{\cmg@t@r}{m}{n}%
      \noexpand\SetMathAlphabet{\noexpand\mathrm}{bold}{OT1}{\cmg@t@r}{bx}{n}%
      \noexpand\SetMathAlphabet{\noexpand\mathbf}{normal}{OT1}{\cmg@t@r}{bx}{n}%
      \noexpand\SetMathAlphabet{\noexpand\mathbf}{bold}{OT1}{\cmg@t@r}{bx}{n}%
      \noexpand\SetMathAlphabet{\noexpand\mathit}{normal}{OT1}{\cmg@t@r}{m}{it}%
      \noexpand\SetMathAlphabet{\noexpand\mathit}{bold}{OT1}{\cmg@t@r}{bx}{it}%
      \noexpand\SetMathAlphabet{\noexpand\mathsf}{normal}{OT1}{\cmg@t@ss}{m}{n}%
      \noexpand\SetMathAlphabet{\noexpand\mathsf}{bold}{OT1}{\cmg@t@ss}{bx}{n}%
      \noexpand\SetMathAlphabet{\noexpand\mathtt}{normal}{OT1}{\cmg@t@tt}{m}{n}%
      \noexpand\SetMathAlphabet{\noexpand\mathtt}{bold}{OT1}{\cmg@t@tt}{bx}{n}%
    }\x
  }%
\fi

\endinput
