Module inkfill.xref

Cross-reference helpers.

Expand source code
"""Cross-reference helpers."""

# std
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import field
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union
import re

# pkg
from .registry import Registrable
from .numerals import NumFormat
from .numerals import DECIMAL

RE_NON_SLUG = re.compile(r"[^a-z0-9_-]")
"""Match characters to convert to dash in a slug."""

RE_REPEATED_DASH = re.compile(r"-{2,}")
"""Match repeated dashes."""


def slugify(*names: str) -> str:
    """Return a slug version of a name."""
    return RE_REPEATED_DASH.sub(
        "-", RE_NON_SLUG.sub("-", "-".join(names).lower())
    ).strip("-")


@dataclass
class RefFormat(Registrable):
    """Reference format."""

    name: str
    """Reference format name."""

    render: Callable[[Ref], str]
    """Render the reference."""

    def __call__(self, ref: Ref) -> str:
        """Render the reference."""
        return self.render(ref)


def cite_name(ref: Ref) -> str:
    """Return citation format for most section headings."""
    cite = ref.cite
    if "." not in cite and not cite.endswith(")"):
        cite += "."
    return f"{cite} {ref.name}".strip()


DOUBLE_QUOTES = RefFormat(
    "double-quotes", lambda ref: f"“{ref.name}”"
).add()
"""Enclose `name` in double quotes (e.g., define a `Term`)."""

TWO_LINE = RefFormat(
    "two-line", lambda ref: f"{ref.kind} {ref.cite}<br />{ref.name}"
).add()
"""Show `kind`, `cite`, and `name` on two lines."""

CITE_NAME = RefFormat("cite-name", cite_name).add()
"""Show full citation and the name (e.g., define `Part`, `Section`, `Subsection`)."""

CITE_LAST = RefFormat(
    "cite-last", lambda ref: ref.numerals[-1].render(ref.values[-1], True)
).add()
"""Show only the end of the citation (e.g., define `Paragraph`, `Clause`)."""

RefFormat.DEFAULT = CITE_FULL = RefFormat(
    "kind-cite", lambda ref: f"{ref.kind} {ref.cite}"
).add()
"""Show `kind` and `cite` (e.g., refer to non-`Term`)."""

NAME_ONLY = RefFormat("name-only", lambda ref: ref.name).add()
"""Show only the name (e.g., refer to a `Term`)."""


@dataclass
class Division(Registrable):
    """Document section."""

    name: str
    """Name of this kind."""

    numeral: NumFormat = field(default=DECIMAL)
    """Default numeral formatting for this kind."""

    define: RefFormat = field(default=CITE_NAME)
    """Default formatting for defining a reference of this type."""

    refer: RefFormat = field(default=CITE_FULL)
    """Default formatting for referring to a reference of this type."""

    def __str__(self) -> str:
        return self.name


# https://weagree.com/clm/contracts/contract-structure-and-presentation/articles-sections-clause-numbering/

Term = Division("Term", NumFormat.get(""), define=NAME_ONLY, refer=NAME_ONLY).add()
"""Defined term."""

Article = Division("Article", NumFormat.get("upper-roman"), define=TWO_LINE).add()
"""Often the top-most level."""

Part = Division("Part", NumFormat.get("upper-alpha")).add()

Division.DEFAULT = Section = Division("Section").add()
"""The most common and default level."""

Subsection = Division("Subsection").add()
"""Subdivision of a section."""

Paragraph = Division("Paragraph", NumFormat.get("lower-alpha"), define=CITE_LAST).add()
"""Items within a subsection."""

Item = Division("Item", NumFormat.get("lower-alpha"), define=CITE_LAST).add()
"""Similar to a paragraph."""

Clause = Division("Clause", NumFormat.get("lower-roman"), define=CITE_LAST).add()
"""Items within a paragraph."""

## Supplementary

Exhibit = Division("Exhibit", NumFormat.get("upper-alpha"), define=TWO_LINE).add()
"""Supporting document."""

Appendix = Division("Appendix", NumFormat.get("upper-alpha"), define=TWO_LINE).add()
"""Additional details."""

Annex = Division("Annex", NumFormat.get("upper-alpha"), define=TWO_LINE).add()
"""Substantial appendix."""

Schedule = Division("Schedule", NumFormat.get("upper-alpha"), define=TWO_LINE).add()
"""Supplementary material."""


@dataclass
class Ref:
    """Reference to a term or section of a document."""

    # `name` and `slug` are for referencing
    # `kind` and formats are for levels/sectioning.

    name: str = ""
    """Term or section title."""

    slug: str = ""
    """Unique ID of this reference (should include `kind` + `name`)."""

    kind: Division = field(default=Section)
    """What kind of reference is this?"""

    values: List[int] = field(default_factory=list)
    """Citation numeral values."""

    numerals: List[NumFormat] = field(default_factory=list)
    """Citation numeral formats."""

    defines: List[RefFormat] = field(default_factory=list)
    """Definition formats."""

    refers: List[RefFormat] = field(default_factory=list)
    """Reference formats."""

    is_defined: bool = False
    """Whether or not this reference has been defined."""

    def copy(self) -> Ref:
        """Return a copy of this reference."""
        return Ref(
            name=self.name,
            slug=self.slug,
            kind=self.kind,
            values=self.values.copy(),
            numerals=self.numerals.copy(),
            defines=self.defines.copy(),
            refers=self.refers.copy(),
        )

    def update_slug(self, slug: str = "") -> Ref:
        """Update the slug when the name or citation has changed."""
        if slug:
            self.slug = slugify(slug)
        elif self.name:
            self.slug = slugify(f"{self.kind}-{self.name}")
        else:
            self.slug = slugify(f"{self.kind}-{self.cite}")
        return self

    @property
    def cite(self) -> str:
        """Return a citation (without the `kind`) to this reference."""
        return "".join(
            num(val, idx > 0)
            for idx, (val, num) in enumerate(zip(self.values, self.numerals))
        )

    @property
    def above_below(self) -> str:
        """Return 'above' if defined, otherwise 'below'."""
        return "above" if self.is_defined else "below"

    def define(self) -> str:
        """Return the reference definition."""
        if self.is_defined:
            raise Exception(f"{self.kind} {self.slug} already defined")
        self.is_defined = True

        refer = self.kind.refer if len(self.refers) == 0 else self.refers[-1]
        define = self.kind.define if len(self.defines) == 0 else self.defines[-1]
        return f"""<span class="def" id="{self.slug}" data-kind="{self.kind}"
                    data-cite="{refer(self).strip()}">{define(self).strip()}</span>"""

    def refer(self) -> str:
        """Return a link to the reference definition."""
        refer = self.kind.refer if len(self.refers) == 0 else self.refers[-1]
        return f"""<a class="ref" href="#{self.slug}"
                    data-kind="{self.kind}">{refer(self).strip()}</a>"""
        # return f"""<a class="ref" href="#{self.slug}" data-kind="{self.kind}"></a>"""

    __str__ = refer


class Refs:
    """Reference manager."""

    stack: List[Ref]
    """Current document levels."""

    store: Dict[str, Ref]
    """Slugs mapped to references."""

    def __init__(self) -> None:
        """Construct a new reference manager."""
        self.reset()

    def reset(self) -> Refs:
        """Reset the references."""
        self.stack = []
        self.store = {}
        return self

    @property
    def undefined(self) -> List[Ref]:
        """References that were never defined."""
        return [ref for ref in self.store.values() if not ref.is_defined]

    @property
    def current(self) -> Ref:
        """Reference to current level."""
        return Ref() if len(self.stack) == 0 else self.stack[-1]

    def push(
        self,
        kind: Union[str, Division] = Section,
        # formatting overrides
        numeral: Union[str, NumFormat, None] = None,
        define: Union[str, RefFormat, None] = None,
        refer: Union[str, RefFormat, None] = None,
    ) -> Refs:
        """Add another level."""
        level = self.current.copy()
        level.kind = Division.get(kind) or Section
        level.values.append(0)
        level.numerals.append(NumFormat.get(numeral, level.kind.numeral))
        level.defines.append(RefFormat.get(define, level.kind.define))
        level.refers.append(RefFormat.get(refer, level.kind.refer))
        self.stack.append(level)
        return self

    def up(self, name: str = "", slug: str = "") -> Ref:
        """Increment current level."""
        ref = self.current.copy()
        ref.name = name
        ref.values[-1] += 1
        ref.update_slug(slug)  # needs name & values updated
        self.stack[-1] = ref
        self.store[ref.slug] = ref
        return ref

    def pop(self, num: int = 1) -> Refs:
        """Remove one or more level."""
        for _ in range(num):
            if len(self.stack) == 0:
                break
            self.stack.pop()
        return self

    ## Short-hand
    def add(self, ref: Ref) -> Ref:
        """Add a ref to the store."""
        self.store[ref.slug] = ref
        return ref

    def term(self, name: str) -> Ref:
        """Refer to a terms."""
        slug = slugify("term", name)
        if slug in self.store:
            return self.store[slug]
        return self.add(Ref(name=name, kind=Term).update_slug())

    def see(self, name: str = "", kind: str = "Section", slug: str = "") -> Ref:
        """Refer to a reference."""
        if slug in self.store:
            return self.store[slug]
        return self.add(Ref(name=name, kind=Division.get(kind)).update_slug(slug))

Global variables

var RE_NON_SLUG

Match characters to convert to dash in a slug.

var RE_REPEATED_DASH

Match repeated dashes.

var DOUBLE_QUOTES : str

Enclose name in double quotes (e.g., define a Term).

var TWO_LINE : str

Show kind, cite, and name on two lines.

var CITE_NAME : str

Show full citation and the name (e.g., define Part, Section, Subsection).

var CITE_LAST : str

Show only the end of the citation (e.g., define Paragraph, Clause).

var NAME_ONLY : str

Show only the name (e.g., refer to a Term).

var Term

Defined term.

var Article

Often the top-most level.

var Subsection

Subdivision of a section.

var Paragraph

Items within a subsection.

var Item

Similar to a paragraph.

var Clause

Items within a paragraph.

var Exhibit

Supporting document.

var Appendix

Additional details.

var Annex

Substantial appendix.

var Schedule

Supplementary material.

Functions

def slugify(*names: str) ‑> str

Return a slug version of a name.

Expand source code
def slugify(*names: str) -> str:
    """Return a slug version of a name."""
    return RE_REPEATED_DASH.sub(
        "-", RE_NON_SLUG.sub("-", "-".join(names).lower())
    ).strip("-")
def cite_name(ref: Ref) ‑> str

Return citation format for most section headings.

Expand source code
def cite_name(ref: Ref) -> str:
    """Return citation format for most section headings."""
    cite = ref.cite
    if "." not in cite and not cite.endswith(")"):
        cite += "."
    return f"{cite} {ref.name}".strip()

Classes

class RefFormat (name: str, render: Callable[[Ref], str])

Reference format.

Expand source code
class RefFormat(Registrable):
    """Reference format."""

    name: str
    """Reference format name."""

    render: Callable[[Ref], str]
    """Render the reference."""

    def __call__(self, ref: Ref) -> str:
        """Render the reference."""
        return self.render(ref)

Ancestors

Class variables

var name : str

Inherited from: Registrable.name

Name of this object.

var render : Callable[[Ref], str]

Render the reference.

var DEFAULTRegistrable

Inherited from: Registrable.DEFAULT

Default value of this type.

Static methods

def get(name: Union[str, T], default: Optional[T] = None) ‑> ~T

Inherited from: Registrable.get

Get the requested named object.

Methods

def add(self: T, override: bool = False) ‑> ~T

Inherited from: Registrable.add

Add this name to the registry.

class Division (name: str, numeral: NumFormat = NumFormat(name='decimal', numeral=<function to_decimal>, prefix='.', suffix=''), define: RefFormat = RefFormat(name='cite-name', render=<function cite_name>), refer: RefFormat = RefFormat(name='kind-cite', render=<function <lambda>>))

Document section.

Expand source code
class Division(Registrable):
    """Document section."""

    name: str
    """Name of this kind."""

    numeral: NumFormat = field(default=DECIMAL)
    """Default numeral formatting for this kind."""

    define: RefFormat = field(default=CITE_NAME)
    """Default formatting for defining a reference of this type."""

    refer: RefFormat = field(default=CITE_FULL)
    """Default formatting for referring to a reference of this type."""

    def __str__(self) -> str:
        return self.name

Ancestors

Class variables

var name : str

Inherited from: Registrable.name

Name of this object.

var numeralNumFormat

Default numeral formatting for this kind.

var defineRefFormat

Default formatting for defining a reference of this type.

var referRefFormat

Default formatting for referring to a reference of this type.

var DEFAULTRegistrable

Inherited from: Registrable.DEFAULT

Default value of this type.

Static methods

def get(name: Union[str, T], default: Optional[T] = None) ‑> ~T

Inherited from: Registrable.get

Get the requested named object.

Methods

def add(self: T, override: bool = False) ‑> ~T

Inherited from: Registrable.add

Add this name to the registry.

class Ref (name: str = '', slug: str = '', kind: Division = Division(name='Section', numeral=NumFormat(name='decimal', numeral=<function to_decimal>, prefix='.', suffix=''), define=RefFormat(name='cite-name', render=<function cite_name>), refer=RefFormat(name='kind-cite', render=<function <lambda>>)), values: List[int] = <factory>, numerals: List[NumFormat] = <factory>, defines: List[RefFormat] = <factory>, refers: List[RefFormat] = <factory>, is_defined: bool = False)

Reference to a term or section of a document.

Expand source code
class Ref:
    """Reference to a term or section of a document."""

    # `name` and `slug` are for referencing
    # `kind` and formats are for levels/sectioning.

    name: str = ""
    """Term or section title."""

    slug: str = ""
    """Unique ID of this reference (should include `kind` + `name`)."""

    kind: Division = field(default=Section)
    """What kind of reference is this?"""

    values: List[int] = field(default_factory=list)
    """Citation numeral values."""

    numerals: List[NumFormat] = field(default_factory=list)
    """Citation numeral formats."""

    defines: List[RefFormat] = field(default_factory=list)
    """Definition formats."""

    refers: List[RefFormat] = field(default_factory=list)
    """Reference formats."""

    is_defined: bool = False
    """Whether or not this reference has been defined."""

    def copy(self) -> Ref:
        """Return a copy of this reference."""
        return Ref(
            name=self.name,
            slug=self.slug,
            kind=self.kind,
            values=self.values.copy(),
            numerals=self.numerals.copy(),
            defines=self.defines.copy(),
            refers=self.refers.copy(),
        )

    def update_slug(self, slug: str = "") -> Ref:
        """Update the slug when the name or citation has changed."""
        if slug:
            self.slug = slugify(slug)
        elif self.name:
            self.slug = slugify(f"{self.kind}-{self.name}")
        else:
            self.slug = slugify(f"{self.kind}-{self.cite}")
        return self

    @property
    def cite(self) -> str:
        """Return a citation (without the `kind`) to this reference."""
        return "".join(
            num(val, idx > 0)
            for idx, (val, num) in enumerate(zip(self.values, self.numerals))
        )

    @property
    def above_below(self) -> str:
        """Return 'above' if defined, otherwise 'below'."""
        return "above" if self.is_defined else "below"

    def define(self) -> str:
        """Return the reference definition."""
        if self.is_defined:
            raise Exception(f"{self.kind} {self.slug} already defined")
        self.is_defined = True

        refer = self.kind.refer if len(self.refers) == 0 else self.refers[-1]
        define = self.kind.define if len(self.defines) == 0 else self.defines[-1]
        return f"""<span class="def" id="{self.slug}" data-kind="{self.kind}"
                    data-cite="{refer(self).strip()}">{define(self).strip()}</span>"""

    def refer(self) -> str:
        """Return a link to the reference definition."""
        refer = self.kind.refer if len(self.refers) == 0 else self.refers[-1]
        return f"""<a class="ref" href="#{self.slug}"
                    data-kind="{self.kind}">{refer(self).strip()}</a>"""
        # return f"""<a class="ref" href="#{self.slug}" data-kind="{self.kind}"></a>"""

    __str__ = refer

Class variables

var values : List[int]

Citation numeral values.

var numerals : List[NumFormat]

Citation numeral formats.

var defines : List[RefFormat]

Definition formats.

var refers : List[RefFormat]

Reference formats.

var name : str

Term or section title.

var slug : str

Unique ID of this reference (should include kind + name).

var kindDivision

What kind of reference is this?

var is_defined : bool

Whether or not this reference has been defined.

Instance variables

var cite : str

Return a citation (without the kind) to this reference.

Expand source code
@property
def cite(self) -> str:
    """Return a citation (without the `kind`) to this reference."""
    return "".join(
        num(val, idx > 0)
        for idx, (val, num) in enumerate(zip(self.values, self.numerals))
    )
var above_below : str

Return 'above' if defined, otherwise 'below'.

Expand source code
@property
def above_below(self) -> str:
    """Return 'above' if defined, otherwise 'below'."""
    return "above" if self.is_defined else "below"

Methods

def copy(self) ‑> Ref

Return a copy of this reference.

Expand source code
def copy(self) -> Ref:
    """Return a copy of this reference."""
    return Ref(
        name=self.name,
        slug=self.slug,
        kind=self.kind,
        values=self.values.copy(),
        numerals=self.numerals.copy(),
        defines=self.defines.copy(),
        refers=self.refers.copy(),
    )
def update_slug(self, slug: str = '') ‑> Ref

Update the slug when the name or citation has changed.

Expand source code
def update_slug(self, slug: str = "") -> Ref:
    """Update the slug when the name or citation has changed."""
    if slug:
        self.slug = slugify(slug)
    elif self.name:
        self.slug = slugify(f"{self.kind}-{self.name}")
    else:
        self.slug = slugify(f"{self.kind}-{self.cite}")
    return self
def define(self) ‑> str

Return the reference definition.

Expand source code
def define(self) -> str:
    """Return the reference definition."""
    if self.is_defined:
        raise Exception(f"{self.kind} {self.slug} already defined")
    self.is_defined = True

    refer = self.kind.refer if len(self.refers) == 0 else self.refers[-1]
    define = self.kind.define if len(self.defines) == 0 else self.defines[-1]
    return f"""<span class="def" id="{self.slug}" data-kind="{self.kind}"
                data-cite="{refer(self).strip()}">{define(self).strip()}</span>"""
def refer(self) ‑> str

Return a link to the reference definition.

Expand source code
def refer(self) -> str:
    """Return a link to the reference definition."""
    refer = self.kind.refer if len(self.refers) == 0 else self.refers[-1]
    return f"""<a class="ref" href="#{self.slug}"
                data-kind="{self.kind}">{refer(self).strip()}</a>"""
    # return f"""<a class="ref" href="#{self.slug}" data-kind="{self.kind}"></a>"""
class Refs

Reference manager.

Construct a new reference manager.

Expand source code
class Refs:
    """Reference manager."""

    stack: List[Ref]
    """Current document levels."""

    store: Dict[str, Ref]
    """Slugs mapped to references."""

    def __init__(self) -> None:
        """Construct a new reference manager."""
        self.reset()

    def reset(self) -> Refs:
        """Reset the references."""
        self.stack = []
        self.store = {}
        return self

    @property
    def undefined(self) -> List[Ref]:
        """References that were never defined."""
        return [ref for ref in self.store.values() if not ref.is_defined]

    @property
    def current(self) -> Ref:
        """Reference to current level."""
        return Ref() if len(self.stack) == 0 else self.stack[-1]

    def push(
        self,
        kind: Union[str, Division] = Section,
        # formatting overrides
        numeral: Union[str, NumFormat, None] = None,
        define: Union[str, RefFormat, None] = None,
        refer: Union[str, RefFormat, None] = None,
    ) -> Refs:
        """Add another level."""
        level = self.current.copy()
        level.kind = Division.get(kind) or Section
        level.values.append(0)
        level.numerals.append(NumFormat.get(numeral, level.kind.numeral))
        level.defines.append(RefFormat.get(define, level.kind.define))
        level.refers.append(RefFormat.get(refer, level.kind.refer))
        self.stack.append(level)
        return self

    def up(self, name: str = "", slug: str = "") -> Ref:
        """Increment current level."""
        ref = self.current.copy()
        ref.name = name
        ref.values[-1] += 1
        ref.update_slug(slug)  # needs name & values updated
        self.stack[-1] = ref
        self.store[ref.slug] = ref
        return ref

    def pop(self, num: int = 1) -> Refs:
        """Remove one or more level."""
        for _ in range(num):
            if len(self.stack) == 0:
                break
            self.stack.pop()
        return self

    ## Short-hand
    def add(self, ref: Ref) -> Ref:
        """Add a ref to the store."""
        self.store[ref.slug] = ref
        return ref

    def term(self, name: str) -> Ref:
        """Refer to a terms."""
        slug = slugify("term", name)
        if slug in self.store:
            return self.store[slug]
        return self.add(Ref(name=name, kind=Term).update_slug())

    def see(self, name: str = "", kind: str = "Section", slug: str = "") -> Ref:
        """Refer to a reference."""
        if slug in self.store:
            return self.store[slug]
        return self.add(Ref(name=name, kind=Division.get(kind)).update_slug(slug))

Class variables

var stack : List[Ref]

Current document levels.

var store : Dict[str, Ref]

Slugs mapped to references.

Instance variables

var undefined : List[Ref]

References that were never defined.

Expand source code
@property
def undefined(self) -> List[Ref]:
    """References that were never defined."""
    return [ref for ref in self.store.values() if not ref.is_defined]
var currentRef

Reference to current level.

Expand source code
@property
def current(self) -> Ref:
    """Reference to current level."""
    return Ref() if len(self.stack) == 0 else self.stack[-1]

Methods

def reset(self) ‑> Refs

Reset the references.

Expand source code
def reset(self) -> Refs:
    """Reset the references."""
    self.stack = []
    self.store = {}
    return self
def push(self, kind: Union[str, Division] = Division(name='Section', numeral=NumFormat(name='decimal', numeral=<function to_decimal>, prefix='.', suffix=''), define=RefFormat(name='cite-name', render=<function cite_name>), refer=RefFormat(name='kind-cite', render=<function <lambda>>)), numeral: Union[str, NumFormat, None] = None, define: Union[str, RefFormat, None] = None, refer: Union[str, RefFormat, None] = None) ‑> Refs

Add another level.

Expand source code
def push(
    self,
    kind: Union[str, Division] = Section,
    # formatting overrides
    numeral: Union[str, NumFormat, None] = None,
    define: Union[str, RefFormat, None] = None,
    refer: Union[str, RefFormat, None] = None,
) -> Refs:
    """Add another level."""
    level = self.current.copy()
    level.kind = Division.get(kind) or Section
    level.values.append(0)
    level.numerals.append(NumFormat.get(numeral, level.kind.numeral))
    level.defines.append(RefFormat.get(define, level.kind.define))
    level.refers.append(RefFormat.get(refer, level.kind.refer))
    self.stack.append(level)
    return self
def up(self, name: str = '', slug: str = '') ‑> Ref

Increment current level.

Expand source code
def up(self, name: str = "", slug: str = "") -> Ref:
    """Increment current level."""
    ref = self.current.copy()
    ref.name = name
    ref.values[-1] += 1
    ref.update_slug(slug)  # needs name & values updated
    self.stack[-1] = ref
    self.store[ref.slug] = ref
    return ref
def pop(self, num: int = 1) ‑> Refs

Remove one or more level.

Expand source code
def pop(self, num: int = 1) -> Refs:
    """Remove one or more level."""
    for _ in range(num):
        if len(self.stack) == 0:
            break
        self.stack.pop()
    return self
def add(self, ref: Ref) ‑> Ref

Add a ref to the store.

Expand source code
def add(self, ref: Ref) -> Ref:
    """Add a ref to the store."""
    self.store[ref.slug] = ref
    return ref
def term(self, name: str) ‑> Ref

Refer to a terms.

Expand source code
def term(self, name: str) -> Ref:
    """Refer to a terms."""
    slug = slugify("term", name)
    if slug in self.store:
        return self.store[slug]
    return self.add(Ref(name=name, kind=Term).update_slug())
def see(self, name: str = '', kind: str = 'Section', slug: str = '') ‑> Ref

Refer to a reference.

Expand source code
def see(self, name: str = "", kind: str = "Section", slug: str = "") -> Ref:
    """Refer to a reference."""
    if slug in self.store:
        return self.store[slug]
    return self.add(Ref(name=name, kind=Division.get(kind)).update_slug(slug))