diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/amalgamate.py | 94 | ||||
| -rw-r--r-- | tools/generate_unicode.py | 117 |
2 files changed, 211 insertions, 0 deletions
diff --git a/tools/amalgamate.py b/tools/amalgamate.py new file mode 100644 index 0000000..f2cdf3b --- /dev/null +++ b/tools/amalgamate.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Write the term core as one term.c and one term.h. + +term.h joins the public headers. term.c joins every source file and puts +each private header in at its first include. A host compiles term.c once, +with any C11 compiler, and links libm. + + amalgamate.py TERM_DIR OUT_DIR [DEPFILE] + +TERM_DIR is the directory with src/ and include/. DEPFILE lists every file +read, for the build system. +""" + +import re +import sys +from pathlib import Path + +INCLUDE = re.compile(r'^\s*#\s*include\s+"([^"]+)"') +PUBLIC = ("term_types.h", "term_input.h", "term.h") + + +class Joiner: + def __init__(self, root: Path): + self.root = root + # The sources find the public headers without the term/ prefix. + self.search = (root / "src", root / "include" / "term") + self.seen: set[Path] = set() + + def resolve(self, name: str, base: Path) -> Path: + for d in (base,) + self.search: + p = (d / name).resolve() + if p.is_file(): + return p + raise SystemExit(f"{base}: cannot find {name}") + + def emit(self, path: Path, out: list[str]) -> None: + """Copy path to out. Replace each local include with the file, + the first time only.""" + self.seen.add(path) + rel = path.relative_to(self.root) + out.append(f"/* ---- {rel} ---- */\n") + for line in path.read_text().splitlines(keepends=True): + m = INCLUDE.match(line) + if not m: + out.append(line) + continue + dep = self.resolve(m.group(1), path.parent) + if dep not in self.seen: + self.emit(dep, out) + out.append(f"/* ---- end {rel} ---- */\n") + + +def sources(src: Path) -> list[Path]: + files = sorted(src.glob("*.c")) + sorted((src / "osc").glob("*.c")) + return [f.resolve() for f in files] + + +def main() -> int: + if len(sys.argv) not in (3, 4): + print(__doc__, file=sys.stderr) + return 2 + root = Path(sys.argv[1]).resolve() + public = root / "include" / "term" + out_dir = Path(sys.argv[2]) + out_dir.mkdir(parents=True, exist_ok=True) + banner = "/* Generated by tools/amalgamate.py. Do not edit. */\n" + + header = Joiner(root) + text = [banner] + header.emit(public / "term.h", text) + (out_dir / "term.h").write_text("".join(text)) + + body = Joiner(root) + text = [banner, + "/* The private headers have helpers that only some platforms or\n" + " * callers outside the core use. Clang warns about them here. */\n" + "#if defined(__clang__)\n" + '#pragma clang diagnostic ignored "-Wunused-function"\n' + "#endif\n", + '#include "term.h"\n'] + # The public headers are in term.h, so the sources skip them. + body.seen.update((public / name).resolve() for name in PUBLIC) + for src in sources(root / "src"): + body.emit(src, text) + (out_dir / "term.c").write_text("".join(text)) + if len(sys.argv) == 4: + read = sorted(header.seen | body.seen) + deps = " ".join(str(p).replace(" ", "\\ ") for p in read) + Path(sys.argv[3]).write_text(f"{out_dir / 'term.c'}: {deps}\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/generate_unicode.py b/tools/generate_unicode.py new file mode 100644 index 0000000..cf72b87 --- /dev/null +++ b/tools/generate_unicode.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +import argparse +import re +import sys +from pathlib import Path + +VERSION_PATTERNS = ( + re.compile(r"^#\s*[A-Za-z-]+-(\d+\.\d+(?:\.\d+)?)\.txt\s*$"), + re.compile(r"^#\s*Version:\s*(\d+\.\d+(?:\.\d+)?)\s*$"), + re.compile(r"^#\s*Used with Emoji Version (\d+\.\d+(?:\.\d+)?)\b"), +) + +def merge(values): + values = sorted(set(values)) + ranges = [] + for cp in values: + if ranges and cp == ranges[-1][1] + 1: + ranges[-1] = (ranges[-1][0], cp) + else: + ranges.append((cp, cp)) + return ranges + +def field_ranges(path, wanted): + values = [] + for raw in path.read_text().splitlines(): + line = raw.split("#", 1)[0].strip() + if not line: + continue + span, prop = (part.strip() for part in line.split(";", 1)) + if prop not in wanted: + continue + ends = [int(x, 16) for x in span.split("..")] + values.extend(range(ends[0], ends[-1] + 1)) + return merge(values) + +def normalize_version(version): + parts = version.split(".") + return ".".join(parts + ["0"] * (3 - len(parts))) + +def header_version(path): + found = set() + for raw in path.read_text().splitlines(): + if not raw.startswith("#"): + break + for pattern in VERSION_PATTERNS: + match = pattern.match(raw) + if match: + found.add(normalize_version(match.group(1))) + if len(found) != 1: + sys.exit(f"{path}: expected one Unicode version in the header, found {sorted(found) or 'none'}") + return found.pop() + +def unicode_categories(path): + by_category = {} + pending = None + for raw in path.read_text().splitlines(): + fields = raw.split(";") + cp, name, category = int(fields[0], 16), fields[1], fields[2] + if name.endswith(", First>"): + pending = (cp, category) + continue + if name.endswith(", Last>"): + start, first_category = pending + assert category == first_category + by_category.setdefault(category, []).extend(range(start, cp + 1)) + pending = None + else: + by_category.setdefault(category, []).append(cp) + assert pending is None + return by_category + +def emit_table(out, name, ranges): + out.append(f"static const struct unicode_range {name}[] = {{") + for i in range(0, len(ranges), 4): + chunk = ranges[i:i + 4] + out.append("\t" + ",".join(f"{{0x{lo:X},0x{hi:X}}}" for lo, hi in chunk) + ",") + out.append("};\n") + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("unicode_data", type=Path) + parser.add_argument("east_asian_width", type=Path) + parser.add_argument("emoji_data", type=Path) + parser.add_argument("output", type=Path) + args = parser.parse_args() + + versions = {path: header_version(path) for path in (args.east_asian_width, args.emoji_data)} + if len(set(versions.values())) != 1: + sys.exit("input files disagree on the Unicode version: " + + ", ".join(f"{path}={version}" for path, version in versions.items())) + version = next(iter(versions.values())) + + categories = unicode_categories(args.unicode_data) + assigned = {cp for cps in categories.values() for cp in cps} + # Conjoining Hangul vowels and finals draw inside the leading consonant. + jamo = list(range(0x1160, 0x1200)) + [cp for cp in range(0xD7B0, 0xD800) if cp in assigned] + zero = merge(categories.get("Mn", []) + categories.get("Me", []) + + categories.get("Cf", []) + jamo) + nonprint = merge(categories.get("Cc", []) + categories.get("Cs", [])) + wide = field_ranges(args.east_asian_width, {"W", "F"}) + spacing = merge(categories.get("Mc", [])) + pictographic = field_ranges(args.emoji_data, {"Extended_Pictographic"}) + emoji_presentation = field_ranges(args.emoji_data, {"Emoji_Presentation"}) + + out = [f"/* Generated from Unicode {version} data by tools/generate_unicode.py. */", "", + f'#define UNICODE_DATA_VERSION "{version}"', ""] + emit_table(out, "zero_width", zero) + emit_table(out, "nonprintable", nonprint) + emit_table(out, "wide", wide) + emit_table(out, "spacing_mark", spacing) + emit_table(out, "extended_pictographic", pictographic) + emit_table(out, "emoji_presentation", emoji_presentation) + args.output.write_text("\n".join(out)) + +if __name__ == "__main__": + main() |
