packages feed

idna2008-1.0.0.0: internal/tools/genIdnaWidth.py

#!/usr/bin/env python3
"""
Generate Text.IDNA2008.Internal.Width.Data from UnicodeData.txt.

Usage:
    python3 internal/tools/genIdnaWidth.py <unicode-version> \\
            <UnicodeData.txt>

Reads field 5 (Decomposition_Mapping) for each codepoint and keeps
those tagged <wide> or <narrow>, emitting two parallel sorted Word32
arrays of (source, target) codepoints for the RFC 5895 section 2.2
fullwidth/halfwidth mapping.

Source: https://www.unicode.org/Public/<version>/ucd/UnicodeData.txt
"""
from __future__ import annotations

import re
import sys

WIDE_NARROW_RE = re.compile(r"^<(wide|narrow)>\s+([0-9A-Fa-f]+)\s*$")


def parse(path: str) -> list[tuple[int, int]]:
    """Return [(source, target)] for every codepoint with a single-
    codepoint <wide> or <narrow> decomposition."""
    out: list[tuple[int, int]] = []
    with open(path, encoding="utf-8") as f:
        for line in f:
            fields = line.rstrip("\n").split(";")
            if len(fields) < 6:
                continue
            cp = int(fields[0], 16)
            decomp = fields[5]
            m = WIDE_NARROW_RE.match(decomp)
            if not m:
                continue
            target = int(m.group(2), 16)
            out.append((cp, target))
    return sorted(out)


def cp_hex(cp: int) -> str:
    return f"0x{cp:06X}"


def emit_word32_list(name: str, items: list[int]) -> str:
    lines = [f"{name} :: ByteArray"]
    if not items:
        lines.append(f"{name} = PBA.byteArrayFromList ([] :: [Word32])")
        return "\n".join(lines)
    lines.append(f"{name} = PBA.byteArrayFromList")
    rendered = []
    for i, v in enumerate(items):
        cell = f"({cp_hex(v)} :: Word32)" if i == 0 else cp_hex(v)
        rendered.append(cell)
    body = "\n    , ".join(rendered)
    lines.append(f"    [ {body}")
    lines.append("    ]")
    return "\n".join(lines)


HEADER = """\
-- |
-- Module      : Text.IDNA2008.Internal.Width.Data
-- Description : Compact codepoint mapping table for the Unicode
--               wide \\/ narrow decompositions consumed by the
--               RFC 5895 section 2.2 (fullwidth\\/halfwidth) mapping.
-- Copyright   : (c) Viktor Dukhovni, 2026
-- License     : BSD-3-Clause
--
-- Maintainer  : ietf-dane@dukhovni.org
-- Stability   : unstable
--
-- AUTOMATICALLY GENERATED.  Do not edit by hand.  Regenerate with:
--
--   python3 internal/tools/genIdnaWidth.py <unicode-version> \\\\
--           <UnicodeData.txt>
--
-- Source: https://www.unicode.org/Public/{version}/ucd/UnicodeData.txt
-- Aligned with: Unicode {version}
-- Width-mapping entries: {n}
--
-- Each entry is a single-codepoint decomposition: a fullwidth or
-- halfwidth codepoint that maps to a single canonical-width target.
-- Stored as two parallel sorted arrays -- 'widthSources' (source
-- codepoints, sorted ascending) and 'widthTargets' (the targets, in
-- the same order).  Lookup is binary search over 'widthSources'.
module Text.IDNA2008.Internal.Width.Data
    ( widthRangeCount
    , widthSources
    , widthTargets
    ) where

import Data.Array.Byte (ByteArray)
import Data.Word (Word32)
import qualified Data.Primitive.ByteArray as PBA

"""


def main() -> None:
    if len(sys.argv) != 3:
        sys.stderr.write(__doc__)
        sys.exit(1)
    version = sys.argv[1]
    pairs = parse(sys.argv[2])
    sources = [s for s, _ in pairs]
    targets = [t for _, t in pairs]

    out = sys.stdout
    out.write(HEADER.format(version=version, n=len(pairs)))

    out.write("-- | Number of (source, target) entries.\n")
    out.write("widthRangeCount :: Int\n")
    out.write(f"widthRangeCount = {len(pairs)}\n\n")

    out.write("-- | Source codepoints, sorted ascending.\n")
    out.write(emit_word32_list("widthSources", sources))
    out.write("\n\n")

    out.write("-- | Target codepoints; parallel to 'widthSources'.\n")
    out.write(emit_word32_list("widthTargets", targets))
    out.write("\n")


if __name__ == "__main__":
    main()