| """ |
| 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} |
|
|