packages feed

idna2008-1.0.0.0: internal/tools/genIdnaProperty.py

#!/usr/bin/env python3
"""
genIdnaProperty.py -- regenerate Text.IDNA2008.Internal.Property.Data
from an IANA IDNA properties CSV.

The IANA registry publishes per-Unicode-version derived-property tables
at:

    https://www.iana.org/assignments/idna-tables-properties/

Concrete CSV download URLs follow the pattern:

    https://www.iana.org/assignments/idna-tables-X.Y.Z/idna-tables-properties.csv

For example (Unicode 12.0.0, the most recent IANA-curated version as of
2024-04-26):

    https://www.iana.org/assignments/idna-tables-12.0.0/idna-tables-properties.csv

Usage:

    python3 internal/tools/genIdnaProperty.py <unicode-version> <csv-path> \\
        > internal/Text/IDNA2008/Internal/Property/Data.hs

The first argument is recorded in the generated module's header for
provenance.  The second is the local path to the IANA CSV.

Each row of the CSV gives a codepoint or codepoint range and one of
five disposition values:

    PVALID, CONTEXTJ, CONTEXTO, DISALLOWED, UNASSIGNED

The output module exposes two parallel arrays:

    rangeStarts :: ByteArray of Word32 -- sorted ascending
    rangeTags   :: ByteArray of Word8  -- one tag per range

with disposition tags 0 = PVALID, 1 = CONTEXTJ, 2 = CONTEXTO,
3 = DISALLOWED, 4 = UNASSIGNED.  Each range covers codepoints
[rangeStarts[i], rangeStarts[i+1] - 1] (or 0x10FFFF for the last
range).
"""
import csv
import sys

DISP_MAP = {
    "PVALID":     0,
    "CONTEXTJ":   1,
    "CONTEXTO":   2,
    "DISALLOWED": 3,
    "UNASSIGNED": 4,
}

STARTS_PER_LINE = 8
TAGS_PER_LINE   = 32


def parse_csv(path):
    starts = []
    tags = []
    with open(path, newline="") as f:
        reader = csv.DictReader(f)
        for row in reader:
            cp = row["Codepoint"]
            prop = row["Property"]
            start = int(cp.split("-")[0], 16)
            try:
                tag = DISP_MAP[prop]
            except KeyError:
                raise SystemExit(f"unknown disposition {prop!r} in {path}")
            starts.append(start)
            tags.append(tag)
    return starts, tags


def emit_module(version, starts, tags, out=sys.stdout):
    n = len(starts)
    p = out.write

    p("-- |\n")
    p("-- Module      : Text.IDNA2008.Internal.Property.Data\n")
    p("-- Description : Codegen'd IDNA2008 codepoint disposition table\n")
    p("-- Copyright   : (c) Viktor Dukhovni, 2026\n")
    p("-- License     : BSD-3-Clause\n")
    p("--\n")
    p("-- Maintainer  : ietf-dane@dukhovni.org\n")
    p("-- Stability   : unstable\n")
    p("--\n")
    p("-- AUTOMATICALLY GENERATED FROM idna-tables-properties.csv.\n")
    p("-- Do not edit by hand.  Regenerate with:\n")
    p("--\n")
    p("--   python3 internal/tools/genIdnaProperty.py <unicode-version> <csv-path>\n")
    p("--\n")
    p("-- Source: https://www.iana.org/assignments/idna-tables-properties/\n")
    p(f"-- Aligned with: Unicode {version}\n")
    p(f"-- Range count: {n}\n")
    p("--\n")
    p("-- Each disposition tag is a small Word8:\n")
    p("--\n")
    p("--   * 0 -- PVALID\n")
    p("--   * 1 -- CONTEXTJ\n")
    p("--   * 2 -- CONTEXTO\n")
    p("--   * 3 -- DISALLOWED\n")
    p("--   * 4 -- UNASSIGNED\n")
    p("module Text.IDNA2008.Internal.Property.Data\n")
    p("    ( rangeCount\n")
    p("    , rangeStarts\n")
    p("    , rangeTags\n")
    p("    ) where\n\n")

    p("import Data.Array.Byte (ByteArray)\n")
    p("import Data.Word (Word8, Word32)\n")
    p("import qualified Data.Primitive.ByteArray as PBA\n\n")

    p("rangeCount :: Int\n")
    p(f"rangeCount = {n}\n\n")

    p("rangeStarts :: ByteArray\n")
    p("rangeStarts = PBA.byteArrayFromList\n")
    for i in range(0, n, STARTS_PER_LINE):
        chunk = starts[i:i + STARTS_PER_LINE]
        if i == 0:
            head = f"(0x{chunk[0]:06X} :: Word32)"
            tail = ", ".join(f"0x{s:06X}" for s in chunk[1:])
            body = head + ((", " + tail) if tail else "")
            p(f"    [ {body}\n")
        else:
            body = ", ".join(f"0x{s:06X}" for s in chunk)
            p(f"    , {body}\n")
    p("    ]\n\n")

    p("rangeTags :: ByteArray\n")
    p("rangeTags = PBA.byteArrayFromList @Word8\n")
    for i in range(0, n, TAGS_PER_LINE):
        chunk = tags[i:i + TAGS_PER_LINE]
        body = ", ".join(str(t) for t in chunk)
        if i == 0:
            p(f"    [ {body}\n")
        else:
            p(f"    , {body}\n")
    p("    ]\n")


def main():
    if len(sys.argv) != 3:
        sys.exit(
            "usage: genIdnaProperty.py <unicode-version> <csv-path>\n"
            "\n"
            "Fetch the CSV from\n"
            "  https://www.iana.org/assignments/idna-tables-X.Y.Z/idna-tables-properties.csv\n"
            "and pass the local path as the second argument."
        )
    version, path = sys.argv[1], sys.argv[2]
    starts, tags = parse_csv(path)
    if not starts:
        sys.exit(f"{path}: no entries parsed")
    emit_module(version, starts, tags)


if __name__ == "__main__":
    main()