|
|
""" |
|
|
sphinx.ext.extlinks |
|
|
~~~~~~~~~~~~~~~~~~~ |
|
|
Extension to save typing and prevent hard-coding of base URLs in the reST |
|
|
files. |
|
|
This adds a new config value called ``extlinks`` that is created like this:: |
|
|
extlinks = {'exmpl': ('https://example.invalid/%s.html', caption), ...} |
|
|
Now you can use e.g. :exmpl:`foo` in your documents. This will create a |
|
|
link to ``https://example.invalid/foo.html``. The link caption depends on |
|
|
the *caption* value given: |
|
|
- If it is ``None``, the caption will be the full URL. |
|
|
- If it is a string, it must contain ``%s`` exactly once. In this case the |
|
|
caption will be *caption* with the role content substituted for ``%s``. |
|
|
You can also give an explicit caption, e.g. :exmpl:`Foo <foo>`. |
|
|
Both, the url string and the caption string must escape ``%`` as ``%%``. |
|
|
:copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. |
|
|
:license: BSD, see LICENSE for details. |
|
|
""" |
|
|
import re |
|
|
from typing import Any, Dict, List, Tuple |
|
|
|
|
|
import sphinx |
|
|
from docutils import nodes, utils |
|
|
from docutils.nodes import Node, system_message |
|
|
from docutils.parsers.rst import Directive |
|
|
from docutils.parsers.rst import directives |
|
|
from sphinx_needs.api import add_dynamic_function |
|
|
from docutils.parsers.rst.states import Inliner |
|
|
from sphinx.application import Sphinx |
|
|
from sphinx.util import caption_ref_re |
|
|
from sphinx.roles import Abbreviation |
|
|
from sphinx.util.docutils import SphinxDirective |
|
|
from sphinx_design.icons import FontawesomeRole, fontawesome |
|
|
|
|
|
|
|
|
|
|
|
class IpaElement(nodes.Element): |
|
|
tagname = 'div' |
|
|
default_ipa_class = 'ipa' |
|
|
def __init__(self, rawsource='', *children, ipa_class='', **attributes): |
|
|
if isinstance(ipa_class, str): |
|
|
ipa_class = [ipa_class] |
|
|
if self.default_ipa_class not in ipa_class: |
|
|
ipa_class.insert(0, self.default_ipa_class) |
|
|
self.ipa_classes = ipa_class |
|
|
self.ipa_classes =[x if x.startswith('ipa') else 'ipa-'+x for x in self.ipa_classes if x] |
|
|
super().__init__(rawsource, *children, **attributes) |
|
|
|
|
|
def for_html(self): |
|
|
return {'class': ' '.join(self.ipa_classes)} |
|
|
|
|
|
class Divider(IpaElement): |
|
|
default_ipa_class = 'divider' |
|
|
def __init__(self): |
|
|
circle = CircleIcon() |
|
|
t = IpaText('',circle) |
|
|
super().__init__('', t) |
|
|
|
|
|
class IpaToolTip(IpaElement): |
|
|
default_ipa_class = 'tooltip' |
|
|
|
|
|
class IpaPhone(IpaElement): |
|
|
default_ipa_class = 'phone' |
|
|
def __init__(self, phone, extra_data, *children, **attributes): |
|
|
ipa_children = [IpaPhoneCheck(phone), IpaPhoneText(phone)] |
|
|
|
|
|
|
|
|
if extra_data: |
|
|
tool_tip_childern = [] |
|
|
for k,v in extra_data.items(): |
|
|
key = IpaToolTipKey(k) |
|
|
if isinstance(v, dict): |
|
|
value = IpaToolTipValue('') |
|
|
line = IpaToolTipLine('', key, value) |
|
|
tool_tip_childern.append(line) |
|
|
for k2, v2 in v.items(): |
|
|
key = IpaToolTipKey(k2, second_level=True, ipa_class=['ipa-second-level']) |
|
|
value = IpaToolTipValue(v2,highlight=phone) |
|
|
line = IpaToolTipLine('', key, value) |
|
|
tool_tip_childern.append(line) |
|
|
|
|
|
else: |
|
|
value = IpaToolTipValue(v) |
|
|
line = IpaToolTipLine('', key, value) |
|
|
tool_tip_childern.append(line) |
|
|
t = IpaToolTip('', *tool_tip_childern) |
|
|
ipa_children.append(t) |
|
|
super().__init__('', *children, *ipa_children, **attributes) |
|
|
|
|
|
class IpaText(IpaElement): |
|
|
tagname = 'span' |
|
|
default_ipa_class = 'text' |
|
|
def __init__(self, text, *children, **attributes): |
|
|
n = [] |
|
|
if text: |
|
|
n.append(nodes.Text(text)) |
|
|
super().__init__('', *n, *children, **attributes) |
|
|
|
|
|
class IpaPhoneCheck(IpaElement): |
|
|
tagname = 'input' |
|
|
default_ipa_class = 'phone-checkbox' |
|
|
def __init__(self, phone, *children, **attributes): |
|
|
super().__init__('', *children, **attributes) |
|
|
self.phone = phone |
|
|
|
|
|
def for_html(self): |
|
|
data = super().for_html() |
|
|
data.update({'type': 'checkbox', 'ids':[f'{self.phone}_checkbox']}) |
|
|
return data |
|
|
|
|
|
class IpaPhoneText(IpaElement): |
|
|
tagname = 'label' |
|
|
default_ipa_class = 'phone-text' |
|
|
def __init__(self, phone, *children, **attributes): |
|
|
self.phone = phone |
|
|
t = IpaText(phone) |
|
|
attributes.update({'for': f'{phone}_checkbox'}) |
|
|
super().__init__('',t, *children, **attributes) |
|
|
|
|
|
def for_html(self): |
|
|
data = super().for_html() |
|
|
data.update({'for':f'{self.phone}_checkbox'}) |
|
|
return data |
|
|
|
|
|
class IpaToolTipLine(IpaElement): |
|
|
default_ipa_class = 'tooltip-line' |
|
|
|
|
|
class IpaToolTipKey(IpaText): |
|
|
default_ipa_class = 'tooltip-key' |
|
|
def __init__(self, key, *children, second_level=False, **attributes): |
|
|
span_children = [] |
|
|
if second_level: |
|
|
span_children.append(nodes.Text(' * ')) |
|
|
|
|
|
|
|
|
key = nodes.Text(f'{key}:') |
|
|
span_children.append(key) |
|
|
super().__init__('', *children, *span_children, **attributes) |
|
|
|
|
|
class IpaToolTipValue(IpaElement): |
|
|
default_ipa_class = 'tooltip-value' |
|
|
def __init__(self, value, *children, highlight=None, **attributes): |
|
|
span_children = [] |
|
|
try: |
|
|
if ',' in value: |
|
|
value = value.replace(',', '') |
|
|
value = int(value) |
|
|
value = f"{value:,}" |
|
|
value = IpaText(value, ipa_class='highlight') |
|
|
span_children.append(value) |
|
|
except ValueError: |
|
|
if highlight is not None: |
|
|
highlight_pattern = re.compile(rf'{highlight}[0-5˩˨˧˦˥ˀ]*') |
|
|
phones = value[1:-1].split(' ') |
|
|
span_children.append(IpaText('[', ipa_class='bracket')) |
|
|
for i, p in enumerate(phones): |
|
|
if highlight_pattern.match(p): |
|
|
span_children.append(IpaText(p, ipa_class=['highlight', 'transcription'])) |
|
|
else: |
|
|
span_children.append(IpaText(p, ipa_class='transcription')) |
|
|
if i != len(phones) - 1: |
|
|
span_children.append(nodes.Text(' ')) |
|
|
span_children.append(IpaText(']', ipa_class='bracket')) |
|
|
|
|
|
super().__init__('', *children, *span_children, **attributes) |
|
|
|
|
|
|
|
|
def checkbox_html_visit(self, node): |
|
|
""" |
|
|
Visitor method for Need-node of builder 'html'. |
|
|
Does only wrap the Need-content into an extra <div> with class=need |
|
|
""" |
|
|
self.body.append(self.starttag(node, node.tagname, **node.for_html())) |
|
|
|
|
|
def checkbox_label_html_visit(self, node): |
|
|
""" |
|
|
Visitor method for Need-node of builder 'html'. |
|
|
Does only wrap the Need-content into an extra <div> with class=need |
|
|
""" |
|
|
self.body.append(self.starttag(node, node.tagname, **node.for_html())) |
|
|
|
|
|
def html_visit(self, node): |
|
|
""" |
|
|
Visitor method for Need-node of builder 'html'. |
|
|
Does only wrap the Need-content into an extra <div> with class=need |
|
|
""" |
|
|
extra = {} |
|
|
if hasattr(node, "for_html"): |
|
|
extra = node.for_html() |
|
|
self.body.append(self.starttag(node, node.tagname, "",**extra)) |
|
|
|
|
|
|
|
|
def html_depart(self, node): |
|
|
self.body.append(f"</{node.tagname}>") |
|
|
|
|
|
class CircleIcon(nodes.Element): |
|
|
tagname = 'i' |
|
|
|
|
|
def for_html(self): |
|
|
return {'class': 'fas fa-circle ipa-dot'} |
|
|
|
|
|
class IpaCell(Directive): |
|
|
has_content = True |
|
|
|
|
|
def run(self): |
|
|
if not self.content: |
|
|
return [IpaElement(ipa_class='empty')] |
|
|
node = nodes.Element() |
|
|
self.state.nested_parse(self.content, self.content_offset, node) |
|
|
output_nodes = [] |
|
|
manner_type = '' |
|
|
for i, row_list in enumerate(node[0]): |
|
|
phone_type = row_list[0].children[0].astext() |
|
|
phone_type_list = row_list[1] |
|
|
phone_type_children = [] |
|
|
if phone_type in {'voiceless', 'voiced'}: |
|
|
manner_type = 'obstruent' |
|
|
elif phone_type in {'unrounded', 'rounded'}: |
|
|
manner_type = 'vowel' |
|
|
else: |
|
|
manner_type = 'sonorant' |
|
|
for phone_list in phone_type_list: |
|
|
phone = phone_list[0].children[0].astext() |
|
|
extra_data = {} |
|
|
if len(phone_list) > 1: |
|
|
for extra_list in phone_list[1:]: |
|
|
for extra in extra_list: |
|
|
base = extra.children[0].astext() |
|
|
if ':' in base: |
|
|
key, value = base.split(': ') |
|
|
extra_data[key] = value |
|
|
else: |
|
|
extra_data[base] = {} |
|
|
for e in extra.children[1:]: |
|
|
for c in e.children: |
|
|
example = c.children[0].astext() |
|
|
key, value = example.split(': ') |
|
|
extra_data[base][key] = value |
|
|
|
|
|
phone_type_children.append(IpaPhone(phone, extra_data)) |
|
|
output_nodes.append(IpaElement('',*phone_type_children, ipa_class=phone_type)) |
|
|
if phone_type in {'voiceless', 'unrounded'}: |
|
|
output_nodes.append(Divider()) |
|
|
if i == len(node[0]) - 1: |
|
|
if phone_type == 'voiceless': |
|
|
output_nodes.append(IpaElement('', ipa_class='voiced')) |
|
|
else: |
|
|
output_nodes.append(IpaElement('', ipa_class='rounded')) |
|
|
elif phone_type in {'voiced', 'rounded'} and i == 0: |
|
|
output_nodes.insert(0,Divider()) |
|
|
if phone_type == 'voiced': |
|
|
output_nodes.insert(0,IpaElement('', ipa_class='voiceless')) |
|
|
else: |
|
|
output_nodes.insert(0,IpaElement('', ipa_class='unrounded')) |
|
|
|
|
|
root_node = IpaElement('', *output_nodes, ipa_class=['cell', manner_type]) |
|
|
return [root_node] |
|
|
|
|
|
icon_short_cuts = { |
|
|
'right-arrow': 'long-arrow-alt-right' |
|
|
} |
|
|
|
|
|
class IpaFontAwesome(FontawesomeRole): |
|
|
def __init__(self): |
|
|
super(IpaFontAwesome, self).__init__('fas') |
|
|
|
|
|
def run(self) -> Tuple[List[nodes.Node], List[nodes.system_message]]: |
|
|
"""Run the role.""" |
|
|
icon, classes = self.text.split(";", 1) if ";" in self.text else [self.text, ""] |
|
|
icon = icon.strip() |
|
|
icon = icon_short_cuts.get(icon, icon) |
|
|
node = fontawesome( |
|
|
icon=icon, classes=[self.style, f"fa-{icon}", 'ipa-icon'] + classes.split() |
|
|
) |
|
|
self.set_source_info(node) |
|
|
return [node], [] |
|
|
|
|
|
|
|
|
def xref( |
|
|
typ: str, |
|
|
rawtext: str, |
|
|
text: str, |
|
|
lineno: int, |
|
|
inliner: Inliner, |
|
|
options: dict = None, |
|
|
content: List[str] = None, |
|
|
) -> Tuple[List[Node], List[system_message]]: |
|
|
|
|
|
title = target = text |
|
|
|
|
|
brace = text.find("<") |
|
|
if brace != -1: |
|
|
m = caption_ref_re.match(text) |
|
|
if m: |
|
|
target = m.group(2) |
|
|
title = m.group(1) |
|
|
else: |
|
|
|
|
|
target = text[brace + 1 :] |
|
|
title = text[:brace] |
|
|
|
|
|
link = xref.links[target] |
|
|
|
|
|
if brace != -1: |
|
|
pnode = nodes.reference(target, title, refuri=link[1]) |
|
|
else: |
|
|
pnode = nodes.reference(target, link[0], refuri=link[1]) |
|
|
|
|
|
return [pnode], [] |
|
|
|
|
|
|
|
|
def get_refs(app): |
|
|
xref.links = app.config.xref_links |
|
|
|
|
|
def setup(app: Sphinx) -> Dict[str, Any]: |
|
|
app.add_config_value("xref_links", {}, "env") |
|
|
app.add_node(IpaPhoneCheck, html=(checkbox_html_visit, html_depart)) |
|
|
app.add_node(IpaPhoneText, html=(checkbox_label_html_visit, html_depart)) |
|
|
app.add_node(CircleIcon, html=(html_visit, html_depart)) |
|
|
app.add_node(Divider, html=(html_visit, html_depart)) |
|
|
app.add_node(IpaToolTip, html=(html_visit, html_depart)) |
|
|
app.add_node(IpaToolTipKey, html=(html_visit, html_depart)) |
|
|
app.add_node(IpaToolTipValue, html=(html_visit, html_depart)) |
|
|
app.add_node(IpaToolTipLine, html=(html_visit, html_depart)) |
|
|
app.add_node(IpaPhone, html=(html_visit, html_depart)) |
|
|
app.add_node(IpaElement, html=(html_visit, html_depart)) |
|
|
app.add_directive("ipa_cell", IpaCell) |
|
|
app.add_role("ipa_icon", IpaFontAwesome()) |
|
|
app.add_role("xref", xref) |
|
|
app.connect("builder-inited", get_refs) |
|
|
return {"version": sphinx.__display_version__, "parallel_read_safe": True} |
|
|
|