"""Pure manifest/address logic, shared by the builder and property tests.""" from __future__ import annotations import json import re IDENT=re.compile(r'[A-Za-z][A-Za-z0-9._-]*\Z') ALIAS=re.compile(r'[A-Za-z][A-Za-z0-9:._-]*\Z') def identifier(value): if not isinstance(value,str) or not IDENT.fullmatch(value):raise ValueError('Invalid ASCII ID: '+repr(value)) return value def private(cp): return type(cp) is int and (0xE000<=cp<=0xF8FF or 0xF0000<=cp<=0xFFFFD or 0x100000<=cp<=0x10FFFD) def allocate(keys,previous=None): keys=list(keys) if len(keys)!=len(set(keys)):raise ValueError('Duplicate glyph IDs') for k in keys:identifier(k) previous=previous or {'schema':'openglyph.allocation.v1','addresses':{}} if previous.get('schema')!='openglyph.allocation.v1':raise ValueError('Unsupported allocation schema') addresses=dict(previous['addresses']) for k,v in addresses.items(): identifier(k) if not private(v):raise ValueError('Invalid font-local private-use address') if len(set(addresses.values()))!=len(addresses):raise ValueError('Conflicting old addresses') used=set(addresses.values()) points=(cp for lo,hi in [(0xE000,0xF8FF),(0xF0000,0xFFFFD),(0x100000,0x10FFFD)] for cp in range(lo,hi+1) if cp not in used) for key in sorted(keys): if key not in addresses:addresses[key]=next(points) return dict(schema='openglyph.allocation.v1',addresses=addresses,retired_keys=sorted(set(addresses)-set(keys))) def validate_project(value): if not isinstance(value,dict) or value.get('schema')!='openglyph.project.v1':raise ValueError('Expected openglyph.project.v1') if set(value)-{'schema','namespace','glyphs'}:raise ValueError('Unknown project fields') namespace=identifier(value.get('namespace')) rows=value.get('glyphs') if not isinstance(rows,list) or not rows:raise ValueError('Project needs nonempty glyphs') ids=[] for row in rows: if not isinstance(row,dict) or not {'id','base','edits'}<=set(row) or set(row)-{'id','base','edits','modules','select'}:raise ValueError('Each glyph needs id, base and edits; optional modules/select') ids.append(identifier(row['id'])) if not isinstance(row['base'],str) or not isinstance(row['edits'],list):raise ValueError('Invalid glyph definition') if len(set(ids))!=len(ids):raise ValueError('Duplicate glyph IDs') return value def catalog_tex(namespace,font_filename,addresses,*,trusted_entries=False): identifier(namespace) if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]*\.(?:otf|ttf)',font_filename):raise ValueError('Unsafe font filename') lines=['% Generated by OpenGlyph; addresses are font-local, not Unicode identity.', '\\OpenGlyphDeclareFont{'+namespace+'}{'+font_filename+'}'] for key,cp in sorted(addresses.items()): identifier(key) if type(cp) is not int or not 0<=cp<=0x10ffff or 0xd800<=cp<=0xdfff:raise ValueError('Invalid scalar') command='\\OpenGlyphDeclareEntry' if trusted_entries else '\\OpenGlyphDeclare' lines.append(command+'{'+namespace+':'+key+'}{'+namespace+'}{'+f'{cp:04X}'+'}') return '\n'.join(lines)+'\n' def read_json(path): def unique(pairs): result={} for k,v in pairs: if k in result:raise ValueError('Duplicate JSON field: '+k) result[k]=v return result return json.loads(path.read_text(encoding='utf-8'),object_pairs_hook=unique, parse_constant=lambda v:(_ for _ in ()).throw(ValueError('Nonfinite JSON number')))