From 1148dd7c30a8dd03a96bb6e3df8853cd5f196aff Mon Sep 17 00:00:00 2001 From: Jorge Marques Date: Wed, 2 Oct 2024 09:59:22 -0300 Subject: [PATCH] Refactor Shell directive, bump version Improve code quality, break it down in multiple methods and use class variables for set once vars. Signed-off-by: Jorge Marques --- adi_doctools/__init__.py | 2 +- adi_doctools/directive/common.py | 264 ++++++++++++++-------- adi_doctools/theme/cosmic/style/code.scss | 2 + docs/docs_guidelines.rst | 20 +- 4 files changed, 181 insertions(+), 107 deletions(-) diff --git a/adi_doctools/__init__.py b/adi_doctools/__init__.py index a79aa7a..9237b24 100644 --- a/adi_doctools/__init__.py +++ b/adi_doctools/__init__.py @@ -12,7 +12,7 @@ from .lut import get_lut from .role.interref import interref_repos_apply -__version__ = "0.3.46" +__version__ = "0.3.47" logger = logging.getLogger(__name__) diff --git a/adi_doctools/directive/common.py b/adi_doctools/directive/common.py index 1613b8b..d8f7f50 100644 --- a/adi_doctools/directive/common.py +++ b/adi_doctools/directive/common.py @@ -13,7 +13,7 @@ from hashlib import sha1 from typing import Tuple -from .node import node_div, node_input, node_label, node_icon, node_source, node_a, node_pre +from .node import node_div, node_input, node_label, node_icon, node_source, node_a from .node import node_iframe, node_video logger = logging.getLogger(__name__) @@ -21,7 +21,7 @@ dft_hide_collapsible_content = True -def parse_rst(state, content, uid: Optional[str]=None): +def parse_rst(state, content, uid: Optional[str] = None): """ Parses rst markup, content can be: * String @@ -67,7 +67,7 @@ def get_descriptions(content): return items def column_entry(self, row, text, node_type: str, classes: List = [], - morecols: int = 0, uid: Optional[str]=None): + morecols: int = 0, uid: Optional[str] = None): attributes = {} if morecols != 0: attributes['morecols'] = morecols @@ -89,7 +89,7 @@ def column_entry(self, row, text, node_type: str, classes: List = [], return row += entry - def column_entries(self, rows, items, uid: Optional[str]=None): + def column_entries(self, rows, items, uid: Optional[str] = None): row = nodes.row() for item in items: if len(item) == 3: @@ -104,7 +104,7 @@ def column_entries(self, rows, items, uid: Optional[str]=None): uid=uid) rows.append(row) - def generic_table(self, description, uid: Optional[str]=None, media_print=False): + def generic_table(self, description, uid: Optional[str] = None, media_print=False): tgroup = nodes.tgroup(cols=2) for _ in range(2): colspec = nodes.colspec(colwidth=1) @@ -349,121 +349,188 @@ class directive_shell(SphinxDirective): required_arguments = 0 optional_arguments = 1 + usr_prefix = None + homedir = None + language = None + win = False + esc = None + def run(self): self.assert_has_content() - shells = ['bash', 'sh', 'zsh', 'ps1'] types = ['$', '/', '~', '#', ' '] + wd = self.get_opts() + wd_ = wd + + line_strip = '' + ll = None + block = [] + parsed = [] + content = list(self.content) + content.append('/') + for line in content: + typ = line[0] if len(line) > 0 else ' ' + line_ = line[1:] if typ in types else line + + wd = self.parse_path(line_strip, wd) + line_strip = line_.strip() + + # Path overwrite + if typ == '/': + wd = line + elif typ == '~' and line_[0] == '/': + wd = self.homedir+line[1:] + + # Block flush condition + # * If the line type change or + # * Is command type and not scaped + # * Is command comment + if ((ll != typ and ll is not None) or + (ll == '$' and block[-1][-1] != self.esc) or + (ll == '#' and len(block) > 0)): + parsed.append((wd_, '\n'.join(block), ll)) + if typ not in ['/', '~']: + block = [line_] + wd_ = wd + else: + block.append(line_) + ll = typ + + literals = node_div( + classes=['code-shell'] + ) + for entry in parsed: + literals += self.block2node(*entry) + + caption = self.options.get('caption') + if caption: + try: + literals = container_wrapper(self, literals, caption) + except ValueError as exc: + document = self.state.document + return [document.reporter.warning(exc, line=self.lineno)] + + self.add_name(literals) + + return [literals] + + def get_opts(self): + """ + Get and sanitize options + """ + shells = ['bash', 'sh', 'zsh', 'ps1'] + user = self.options.get('user') group = self.options.get('group') - caption = self.options.get('caption') - language = self.arguments[0].strip() if len(self.arguments) > 0 else None + if len(self.arguments) > 0: + self.language = self.arguments[0].strip() if user is None: user = "user" if group is None: group = "analog" - if language not in shells: - if language is not None: + if self.language not in shells: + if self.language is not None: location = self.state_machine.get_source_and_line(self.lineno) logger.warning(("shell '%s' is invalid, valid values are " f"[{', '.join(shells)}]."), - language, location=location) - language = "bash" + self.language, location=location) + self.language = "bash" + # Generate static vars + if self.language == 'ps1': + self.win = True + wd = "/c" + self.homedir = f"/c/Users/{user}" + self.esc = '`' + else: + wd = f"/home/{user}" + self.homedir = wd + self.esc = '\\' + if 'showuser' in self.options: - if language != 'ps1': - wd_p = f"{user}@{group}:" + if not self.win: + self.usr_prefix = f"{user}@{group}:" else: - wd_p = f"[{group}:{user}] " + self.usr_prefix = f"{user}.{group} " else: - wd_p = "" - - def resolve_block(l_, l, wd, lang): - if l in ['/', '~']: - return None - - sep = '$' if lang != 'ps1' else '>' - lit = node_div() - if l in ['$', '#']: - if wd is not None: - sep = wd + sep - lit += nodes.literal_block(sep, sep, - classes=['no-select', 'float-left']) - - if l == '$': - lit_ = nodes.literal_block(l_, l_, classes=['bold']) - lit_['language'] = language - lit += lit_ - elif l == '#': - lit_ = nodes.literal_block('#'+l_,'#'+l_) - lit_['language'] = language - lit += lit_ - elif l == ' ': - lit += nodes.literal_block(l_, l_, classes=['no-select']) + self.usr_prefix = "" + return wd + + def parse_path(self, line, wd): + """ + Covert "cd" commands into paths. + For windows, do a fake to bash conversion, similar to cygpath + """ + line = line.split() + if len(line) < 2: + return wd + + for i in range(0, len(line)): + if line[i-1] != "cd": + continue + + p_ = line[i] + if self.win: + p_ = p_.replace('\\', '/') + + if p_[0] == '~': + wd = self.homedir + p_[1:] + elif self.win and p_[1] == ':': + wd = '/'+p_[0]+p_[2:] else: - lit += nodes.literal_block(l_, l_, classes=['no-select']) + wd = path.abspath(path.join(wd, p_)) + if self.win and wd == '/': + wd = "/c" - lit += node_div( - classes = [f"clear-left"] - ) - return lit + if len(wd) > 1 and wd[-1] in ['/', ';']: + wd = wd[:-1] + i += 1 + return wd - literals = node_div( - classes=['code-shell'] - ) - ll = None - if language == 'ps1': - wd = wd_ = '/C:/' - else: - wd = wd_ = '~' - block = [] - cd_flush = False - l__ = '' - for line in self.content: - if l__.startswith("cd "): - if language != 'ps1': - wd = path.abspath(path.join(wd, l__[3:])) - else: - l__ = l__.replace('\\', '/') - if l__[4] == ':': - wd = '/'+l__[3:] - else: - wd = path.abspath(path.join(path.sep, wd, l__[3:])) - if len(wd) > 1 and wd[-1] == "/": - wd = wd[:-1] - - l = line[0] if len(line) > 0 else ' ' - l_ = line[1:] if l in types else line - l__ = l_.strip() - - if language != 'ps1': - if l == '/': - wd = line - elif l == '~': - wd = line - if ((ll != l and ll is not None) or (ll == '$' and block[-1][-1] != '\\') or - ll == '#' and len(block) > 0): - wd__ = wd_ if language != 'ps1' else wd_.replace('/', '\\')[1:] - literals += resolve_block('\n'.join(block), ll, - wd_p+wd__, language) - if l not in ['/', '~']: - block = [l_] - wd_ = wd - else: - block.append(l_) - ll = l - literals += resolve_block('\n'.join(block), ll, wd, language) + def block2node(self, wd, line, typ): + """ + Convert block into node objects + """ + if typ in ['/', '~']: + return None - caption = self.options.get('caption') - if caption: - try: - literals = container_wrapper(self, literals, caption) - except ValueError as exc: - return [document.reporter.warning(exc, line=self.lineno)] + if self.win: + wd = (wd[1].upper()+':'+wd[2:]).replace('/', '\\') - self.add_name(literals) + if not self.win: + sep = '$' + elif len(wd) == 2: + sep = '\\>' + else: + sep = '>' + + lit = node_div() + if typ in ['$', '#']: + if wd is not None: + if wd.startswith(self.homedir): + wd = '~' + wd[len(self.homedir):] + sep = self.usr_prefix + wd + sep + lit += nodes.literal_block(sep, sep, + classes=['no-select', 'float-left']) + + if typ == '$': + lit_ = nodes.literal_block(line, line, classes=['bold']) + lit_['language'] = self.language + elif typ == '#': + lit_ = nodes.literal_block('#'+line, '#'+line) + lit_['language'] = self.language + elif typ == ' ': + lit_ = nodes.literal_block(line, line, classes=['no-select']) + lit_['language'] = 'text' + else: + lit_ = nodes.literal_block(line, line, classes=['no-select']) + lit_['language'] = 'text' + lit += lit_ - return [literals] + lit += node_div( + classes=["clear-left"] + ) + return lit def common_setup(app): @@ -472,4 +539,5 @@ def common_setup(app): app.add_directive('clear-content', directive_clear_content) app.add_directive('shell', directive_shell) - app.add_config_value('hide_collapsible_content', dft_hide_collapsible_content, 'env') + app.add_config_value('hide_collapsible_content', + dft_hide_collapsible_content, 'env') diff --git a/adi_doctools/theme/cosmic/style/code.scss b/adi_doctools/theme/cosmic/style/code.scss index 00776fc..b174ebd 100644 --- a/adi_doctools/theme/cosmic/style/code.scss +++ b/adi_doctools/theme/cosmic/style/code.scss @@ -49,6 +49,7 @@ em.sig-param, em.property { padding: .75em; margin-bottom: .5em; user-select: text; + overflow-x: scroll; .highlight { padding: 0; @@ -63,6 +64,7 @@ em.sig-param, em.property { clear: left; display: inline-block; margin-right: .25em; + color: var(--accent-color); } .bold { diff --git a/docs/docs_guidelines.rst b/docs/docs_guidelines.rst index 783f24b..f451e14 100644 --- a/docs/docs_guidelines.rst +++ b/docs/docs_guidelines.rst @@ -636,8 +636,8 @@ That means, each line is prefixed by character to: * ``$``: bash commands. * :code:`\ ` (one space): command output. * ``#``: bash comments -* ``/``: set absolute working directory (unix only). -* ``~``: set relative to "home" working directory (unix only). +* ``/``: set absolute working directory (cygpath-formatted for ps1). +* ``~``: set relative to "home" working directory (cygpath-formatted for ps1). Anything that does not match the previous characters will default to output print, but please be careful, since you may accidentally mark a working directory or @@ -681,13 +681,15 @@ For Windows, set bash type as ``ps1`` (PowerShell), for example: .. code:: rst .. shell:: ps1 + :user: Analog - $cd C:\Users + /e/MyData + $cd ~/Documents $ls Mode LastWriteTime Name ---- ------------- ---- - d---- 6/14/2024 10:30 AM user1 - d---- 6/14/2024 10:30 AM user2 + d---- 6/14/2024 10:30 AM ImportantFiles + d---- 6/14/2024 10:30 AM LessImportantFiles $cd ..\Other\Folder $echo HelloWindows HelloWindows @@ -695,13 +697,15 @@ For Windows, set bash type as ``ps1`` (PowerShell), for example: Renders as: .. shell:: ps1 + :user: Analog - $cd C:\Users + /e/MyData + $cd ~/Documents $ls Mode LastWriteTime Name ---- ------------- ---- - d---- 6/14/2024 10:30 AM user1 - d---- 6/14/2024 10:30 AM user2 + d---- 6/14/2024 10:30 AM ImportantFiles + d---- 6/14/2024 10:30 AM LessImportantFiles $cd ..\Other\Folder $echo HelloWindows HelloWindows