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 aTerm
). var TWO_LINE : str
-
Show
kind
,cite
, andname
on two lines. var CITE_NAME : str
-
Show full citation and the name (e.g., define
Part
,Section
,Subsection
). var CITE_LAST : str
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 DEFAULT : Registrable
-
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 numeral : NumFormat
-
Default numeral formatting for this kind.
var define : RefFormat
-
Default formatting for defining a reference of this type.
var refer : RefFormat
-
Default formatting for referring to a reference of this type.
var DEFAULT : Registrable
-
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 kind : Division
-
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 current : Ref
-
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))