From 64e8ee83ea94b834d1cdd59f0c888bb6440bc1c4 Mon Sep 17 00:00:00 2001 From: Shengyu Zhang Date: Thu, 12 Aug 2021 23:06:17 +0800 Subject: [PATCH 1/7] Don't save doc nodes in snippet anymore --- sphinxnotes/snippet/__init__.py | 241 ++++++++------------- sphinxnotes/snippet/cache.py | 6 +- sphinxnotes/snippet/cli.py | 9 +- sphinxnotes/snippet/ext.py | 82 +++---- sphinxnotes/snippet/integration/plugin.sh | 6 +- sphinxnotes/snippet/integration/plugin.vim | 6 +- sphinxnotes/snippet/picker.py | 131 ++++++----- 7 files changed, 223 insertions(+), 258 deletions(-) diff --git a/sphinxnotes/snippet/__init__.py b/sphinxnotes/snippet/__init__.py index d50549e..3cbe9a8 100644 --- a/sphinxnotes/snippet/__init__.py +++ b/sphinxnotes/snippet/__init__.py @@ -7,194 +7,143 @@ """ from __future__ import annotations -from typing import List, Tuple, Optional, Any, Dict -from dataclasses import dataclass, field -from abc import ABC, abstractclassmethod +from typing import List, Tuple, Optional import itertools from docutils import nodes __title__= 'sphinxnotes-snippet' -__license__ = 'BSD', +__license__ = 'BSD' __version__ = '1.0b6' __author__ = 'Shengyu Zhang' __url__ = 'https://sphinx-notes.github.io/snippet' __description__ = 'Non-intrusive snippet manager for Sphinx documentation' __keywords__ = 'documentation, sphinx, extension, utility' -@dataclass -class Snippet(ABC): +class Snippet(object): """ - Snippet is a {abstract,data}class represents a snippet of reStructuredText - documentation. Note that it is not always continuous fragment at text (rst) - level. - """ - _scope:Tuple[int,int] = field(init=False) - _refid:Optional[str] = field(init=False) - - def __post_init__(self) -> None: - """Post-init processing routine of dataclass""" - - # Calcuate scope before deepcopy - scope = [float('inf'), -float('inf')] - for node in self.nodes(): - if not node.line: - continue # Skip node that have None line, I dont know why :'( - scope[0] = min(scope[0], line_of_start(node)) - scope[1] = max(scope[1], line_of_end(node)) - self._scope = scope - - # Find exactly one id attr in nodes - self._refid = None - for node in self.nodes(): - if node['ids']: - self._refid = node['ids'][0] - break - # If no node has id, use parent's - if not self._refid: - for node in self.nodes(): - if node.parent['ids']: - self._refid = node.parent['ids'][0] - break - - - @abstractclassmethod - def nodes(self) -> List[nodes.Node]: - """Return the out of tree nodes that make up this snippet.""" - pass - - - @abstractclassmethod - def excerpt(self) -> str: - """Return excerpt of snippet (for preview).""" - pass - - - @abstractclassmethod - def kind(self) -> str: - """Return kind of snippet (for filtering).""" - pass - - - def file(self) -> str: - """Return source file path of snippet""" - # All nodes should have same source file - return self.nodes()[0].source - - - def scope(self) -> Tuple[int,int]: - """ - Return the scope of snippet, which corresponding to the line - number in the source file. - - A scope is a left closed and right open interval of the line number - ``[left, right)``. - """ - return self._scope - - - def text(self) -> List[str]: - """Return the original reStructuredText text of snippet.""" - return read_partial_file(self.file(), self.scope()) + Snippet is base class of reStructuredText snippet. + :param nodes: Document nodes that make up this snippet + """ - def refid(self) -> Optional[str]: - """ - Return the possible identifier key of snippet. - It is picked from nodes' (or nodes' parent's) `ids attr`_. + #: Source file path of snippet + file:str - .. _ids attr: https://docutils.sourceforge.io/docs/ref/doctree.html#ids - """ - return self._refid + #: Line number range of snippet, in the source file which is left closed + #: and right opened. + lineno:Tuple[int,int] + #: The original reStructuredText of snippet + rst:List[str] - def __getstate__(self) -> Dict[str,Any]: - """Implement :py:meth:`pickle.object.__getstate__`.""" - return self.__dict__.copy() + #: The possible identifier key of snippet, which is picked from nodes' + #: (or nodes' parent's) `ids attr`_. + #: + #: .. _ids attr: https://docutils.sourceforge.io/docs/ref/doctree.html#ids + refid:Optional[str] + def __init__(self, *nodes:nodes.Node) -> None: + assert len(nodes) != 0 -@dataclass -class Headline(Snippet): - """Documentation title and possible subtitle.""" - title:nodes.title - subtitle:Optional[nodes.title] + self.file = nodes[0].source - def nodes(self) -> List[nodes.Node]: - if not self.subtitle: - return [self.title] - return [self.title, self.subtitle] + lineno = [float('inf'), -float('inf')] + for node in nodes: + if not node.line: + continue # Skip node that have None line, I dont know why + lineno[0] = min(lineno[0], _line_of_start(node)) + lineno[1] = max(lineno[1], _line_of_end(node)) + self.lineno = lineno + + lines = [] + with open(self.file, "r") as f: + start = self.lineno[0] - 1 + stop = self.lineno[1] - 1 + for line in itertools.islice(f, start, stop): + lines.append(line.strip('\n')) + self.rst = lines + + # Find exactly one ID attr in nodes + self.refid = None + for node in nodes: + if node['ids']: + self.refid = node['ids'][0] + break + # If no ID found, try parent + if not self.refid: + for node in nodes: + if node.parent['ids']: + self.refid = node.parent['ids'][0] + break - def excerpt(self) -> str: - if not self.subtitle: - return '<%s>' % self.title.astext() - return '<%s ~%s~>' % (self.title.astext(), self.subtitle.astext()) - @classmethod - def kind(cls) -> str: - return 'd' +class Text(Snippet): + #: Text of snippet + text:str + def __init__(self, node:nodes.Node) -> None: + super().__init__(node) + self.text = node.astext() - def text(self) -> List[str]: - """ - Headline represents a reStructuredText document, - so return the whole source file. - """ - with open(self.file()) as f: - return f.read().splitlines() +class CodeBlock(Text): + #: Language of code block + language:str + #: Caption of code block + caption:Optional[str] - def __getstate__(self) -> Dict[str,Any]: - self.title = self.title.deepcopy() - if self.subtitle: - self.subtitle = self.subtitle.deepcopy() - return super().__getstate__() + def __init__(self, node:nodes.literal_block) -> None: + assert isinstance(node, nodes.literal_block) + super().__init__(node) + self.language = node['language'] + self.caption = node.get('caption') -@dataclass -class Code(Snippet): - """A code block with description.""" - description:List[nodes.Body] - block:nodes.literal_block +class WithCodeBlock(object): + code_blocks:List[CodeBlock] - def nodes(self) -> List[nodes.Node]: - return self.description.copy() + [self.block] + def __init__(self, nodes:nodes.Nodes) -> None: + self.code_blocks = [] + for n in nodes.traverse(nodes.literal_block): + self.code_blocks.append(self.CodeBlock(n)) - def excerpt(self) -> str: - return '/%s/ ' % self.language() + \ - self.description[0].astext().replace('\n', '') +class Title(Text): + def __init__(self, node:nodes.title) -> None: + assert isinstance(node, nodes.title) + super().__init__(node) - @classmethod - def kind(cls) -> str: - return 'c' +class WithTitle(object): + title:Optional[Title] + def __init__(self, node:nodes.Node) -> None: + title_node = node.next_node(nodes.title) + self.title = Title(title_node) if title_node else None - def language(self) -> str: - """Return the (programing) language that appears in code.""" - return self.block['language'] +class Section(Snippet, WithTitle): + def __init__(self, node:nodes.section) -> None: + assert isinstance(node, nodes.section) + Snippet.__init__(self, node) + WithTitle.__init__(self, node) - def __getstate__(self) -> Dict[str,Any]: - self.description = [x.deepcopy() for x in self.description] - self.block = self.block.deepcopy() - return super().__getstate__() +class Document(Section): + def __init__(self, node:nodes.document) -> None: + assert isinstance(node, nodes.document) + super().__init__(node.next_node(nodes.section)) -def read_partial_file(filename:str, scope:Tuple[int,Optional[int]]) -> List[str]: - lines = [] - with open(filename, "r") as f: - start = scope[0] - 1 - stop = scope[1] - 1 if scope[1] else None - for line in itertools.islice(f, start, stop): - lines.append(line.strip('\n')) - return lines +################ +# Nodes helper # +################ -def line_of_start(node:nodes.Node) -> int: +def _line_of_start(node:nodes.Node) -> int: assert node.line if isinstance(node, nodes.title): if isinstance(node.parent.parent, nodes.document): @@ -213,11 +162,11 @@ def line_of_start(node:nodes.Node) -> int: return node.line -def line_of_end(node:nodes.Node) -> Optional[int]: +def _line_of_end(node:nodes.Node) -> Optional[int]: next_node = node.next_node(descend=False, siblings=True, ascend=True) while next_node: if next_node.line: - return line_of_start(next_node) + return _line_of_start(next_node) next_node = next_node.next_node( # Some nodes' line attr is always None, but their children has # valid line attr diff --git a/sphinxnotes/snippet/cache.py b/sphinxnotes/snippet/cache.py index 29577d7..b1265a2 100644 --- a/sphinxnotes/snippet/cache.py +++ b/sphinxnotes/snippet/cache.py @@ -16,6 +16,8 @@ class Item(object): """ Item of snippet cache. """ snippet:Snippet + kinds:List[str] + excerpt:str titlepath:List[str] keywords:List[str] @@ -52,8 +54,8 @@ def post_dump(self, key:DocID, items:List[Item]) -> None: # Add new index to every where for i, item in enumerate(items): index_id = self.gen_index_id() - self.indexes[index_id] = (item.snippet.kind(), - item.snippet.excerpt(), + self.indexes[index_id] = (item.kinds, + item.excerpt, item.titlepath, item.keywords) self.index_id_to_doc_id[index_id] = (key, i) diff --git a/sphinxnotes/snippet/cli.py b/sphinxnotes/snippet/cli.py index f5e195f..cd33806 100644 --- a/sphinxnotes/snippet/cli.py +++ b/sphinxnotes/snippet/cli.py @@ -45,11 +45,10 @@ def main(argv:List[str]=sys.argv[1:]) -> int: formatter_class=HelpFormatter, epilog=dedent(""" snippet kinds: - d (headline) documentation title and possible subtitle - c (code) notes with code block - p (procedure) notes with a sequence of code for doing something (TODO) - i (image) notes with an image (TODO) - * (any) wildcard kind for any kind of snippet""")) + d (document) a complete reST document + s (section) a reST section + c (code) a snippet with code blocks + * (any) wildcard for any snippet""")) parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + __version__) parser.add_argument('-c', '--config', default=DEFAULT_CONFIG_FILE, help='path to configuration file') diff --git a/sphinxnotes/snippet/ext.py b/sphinxnotes/snippet/ext.py index c0843d0..494820a 100644 --- a/sphinxnotes/snippet/ext.py +++ b/sphinxnotes/snippet/ext.py @@ -21,11 +21,11 @@ from sphinx.util import logging from .config import Config -from . import Snippet, Headline, Code -from .picker import pick_doctitle, pick_codes +from . import Snippet, WithTitle, Document, Section, WithCodeBlock +from .picker import pick from .cache import Cache, Item from .keyword import Extractor -from .utils.titlepath import resolve_fullpath, resolve_docpath +from .utils.titlepath import resolve_fullpath from .builder import Builder @@ -34,26 +34,42 @@ cache:Cache = None extractor:Extractor = Extractor() + +def extract_kinds(s:Snippet) -> str: + kinds = '' + if isinstance(s, Document): + kinds += 'd' + elif isinstance(s, Section): + kinds += 's' + if isinstance(s, WithCodeBlock): + kinds += 'c' + return kinds + + +def extract_excerpt(s:Snippet) -> str: + if isinstance(s, WithTitle) and s.title is not None: + return '<' + s.title.text + '>' + return '' + + def extract_keywords(s:Snippet) -> List[str]: + keywords = [] # TODO: Deal with more snippet - if isinstance(s, Code): - return extractor.extract('\n'.join(map(lambda x:x.astext(), s.description)), - top_n=10) - elif isinstance(s, Headline): - return extractor.extract('\n'.join(map(lambda x:x.astext(), s.nodes())), - strip_stopwords=False) - else: - logger.warning('unknown snippet instance %s', s) + if isinstance(s, WithTitle) and s.title is not None: + keywords.extend(extractor.extract(s.title.text, strip_stopwords=False)) + return keywords -def is_matched(pats:Dict[str,List[str]], cls:Type[Snippet], docname:str) -> bool: +def is_matched(pats:Dict[str,List[str]], s:[Snippet], docname:str) -> bool: # Wildcard if '*' in pats: for pat in pats['*']: if re.match(pat, docname): return True - if cls.kind() in pats: - for pat in pats[cls.kind()]: + for k in extract_kinds(s): + if k not in pats: + continue + for pat in pats[k]: if re.match(pat, docname): return True return False @@ -85,33 +101,19 @@ def on_doctree_resolved(app:Sphinx, doctree:nodes.document, docname:str) -> None return pats = app.config.snippet_patterns - matched = False - doc = cache.setdefault((app.config.project, docname), []) - # Pick document title from doctree - if is_matched(pats, Headline, docname): - matched = True - doctitle = pick_doctitle(doctree) - if doctitle: - doc.append(Item(titlepath=resolve_docpath(app.env, - docname, - include_project=True), - snippet=doctitle, - keywords=[docname] + extract_keywords(doctitle))) - - # Pick code snippet from doctree - if is_matched(pats, Code, docname): - matched = True - codes = pick_codes(doctree) - for code in codes: - doc.append(Item(titlepath=resolve_fullpath(app.env, - docname, - code.nodes()[0], - include_project=True), - snippet=code, - keywords=extract_keywords(code))) - - if not matched: + for s, n in pick(doctree): + if not is_matched(pats, s, docname): + continue + doc.append(Item(snippet=s, + kinds=extract_kinds(s), + excerpt=extract_excerpt(s), + keywords=extract_keywords(s), + titlepath=resolve_fullpath(app.env, + docname, + n, + include_project=True))) + if len(doc) == 0: del cache[(app.config.project, docname)] diff --git a/sphinxnotes/snippet/integration/plugin.sh b/sphinxnotes/snippet/integration/plugin.sh index f0ac1ea..82af47f 100644 --- a/sphinxnotes/snippet/integration/plugin.sh +++ b/sphinxnotes/snippet/integration/plugin.sh @@ -16,7 +16,7 @@ function snippet_list() { } function snippet_view() { - index_id=$(snippet_list dc) + index_id=$(snippet_list ds) [ -z "$index_id" ] && return # Make sure we have $PAGER @@ -37,7 +37,7 @@ function snippet_view() { } function snippet_edit() { - index_id=$(snippet_list dc) + index_id=$(snippet_list ds) [ -z "$index_id" ] && return # Make sure we have $EDITOR @@ -58,7 +58,7 @@ function snippet_edit() { } function snippet_url() { - index_id=$(snippet_list dc) + index_id=$(snippet_list ds) [ -z "$index_id" ] && return # Make sure we have $BROWSER diff --git a/sphinxnotes/snippet/integration/plugin.vim b/sphinxnotes/snippet/integration/plugin.vim index b5db0df..ad4362a 100644 --- a/sphinxnotes/snippet/integration/plugin.vim +++ b/sphinxnotes/snippet/integration/plugin.vim @@ -29,7 +29,7 @@ function! g:SphinxNotesSnippetListAndView() function! s:CallView(selection) call g:SphinxNotesSnippetView(s:SplitID(a:selection)) endfunction - call g:SphinxNotesSnippetList(function('s:CallView'), 'dc') + call g:SphinxNotesSnippetList(function('s:CallView'), 'ds') endfunction " https://github.com/anhmv/vim-float-window/blob/master/plugin/float-window.vim @@ -76,7 +76,7 @@ function! g:SphinxNotesSnippetListAndEdit() function! s:CallEdit(selection) call g:SphinxNotesSnippetEdit(s:SplitID(a:selection)) endfunction - call g:SphinxNotesSnippetList(function('s:CallEdit'), 'dc') + call g:SphinxNotesSnippetList(function('s:CallEdit'), 'ds') endfunction function! g:SphinxNotesSnippetUrl(id) @@ -90,7 +90,7 @@ function! g:SphinxNotesSnippetListAndUrl() function! s:CallUrl(selection) call g:SphinxNotesSnippetUrl(s:SplitID(a:selection)) endfunction - call g:SphinxNotesSnippetList(function('s:CallUrl'), 'dc') + call g:SphinxNotesSnippetList(function('s:CallUrl'), 'ds') endfunction " vim: set shiftwidth=2: diff --git a/sphinxnotes/snippet/picker.py b/sphinxnotes/snippet/picker.py index 14a800c..3213401 100644 --- a/sphinxnotes/snippet/picker.py +++ b/sphinxnotes/snippet/picker.py @@ -9,91 +9,104 @@ """ from __future__ import annotations -from typing import List, Optional, Dict +from typing import List, Tuple from docutils import nodes -from . import Headline, Code -from .utils.titlepath import resolve_doctitle +from sphinx.util import logging +from . import Snippet, Section, Document -def pick_doctitle(doctree:nodes.document) -> Optional[Headline]: - """Pick document title and subtitle (if have) from document.""" - title, subtitle = resolve_doctitle(doctree) - if not title: - return None - return Headline(title=title, subtitle=subtitle) +logger = logging.getLogger(__name__) -def pick_codes(doctree:nodes.document) -> List[Code]: - """Pick code snippet from document.""" - picker = CodePicker(doctree) - doctree.walkabout(picker) - return picker.codes +def pick(doctree:nodes.document) -> List[Tuple[Snippet,nodes.section]]: + """ + Pick snippets from document, return a list of snippet and the section + it belongs to. + """ + snippets = [] -class CodePicker(nodes.SparseNodeVisitor): + # Pick document + toplevel_section = doctree.next_node(nodes.section) + if toplevel_section: + snippets.append((Document(doctree), toplevel_section)) + else: + logger.warning('can not pick document without child section: %s', doctree) + + # Pick sections + section_picker = SectionPicker(doctree) + doctree.walkabout(section_picker) + snippets.extend(section_picker.sections) + + return snippets + + +class SectionPicker(nodes.SparseNodeVisitor): """Node visitor for picking code snippet from document.""" - # Code snippets that picked from document - codes:List[Code] - # (container, pointer) that Recording the read pointer inside container - offset:Dict[nodes.Node,int] - # List of unsupported languages (:class:`pygments.lexers.Lexer`) - unsupported_languages:List[str] = ['default'] + + #: Constant list of unsupported languages (:class:`pygments.lexers.Lexer`) + UNSUPPORTED_LANGUAGES:List[str] = ['default'] + + #: List of picked section snippets and the section it belongs to + sections:List[Tuple[Section,nodes.section]] + + _section_has_code_block:bool + _section_level:int def __init__(self, document:nodes.document) -> None: super().__init__(document) - self.codes = [] - self.offset = {} + self.sections = [] + self._section_has_code_block = False + self._section_level = 0 + ################### + # Visitor methods # + ################### def visit_literal_block(self, node:nodes.literal_block) -> None: - if node['language'] in self.unsupported_languages: + if node['language'] in self.UNSUPPORTED_LANGUAGES: raise nodes.SkipNode + self._has_code_block = True - desc = [] - container = node.parent - # Get current offset or use first child of container (must exists) - start = self.offset.get(container) or 0 - # Stop iteration before current literal_block - end = container.index(node) - # Collect description - for i in range(start, end): - if self.is_description(container[i]): - desc.append(container[i]) - i += 1 # Skip literal_block - # Collect continuously post_description - for i in range(i, len(container)): - if self.is_post_description(container[i]): - desc.append(container[i]) - else: - i -= 1 # Currnet node is not post_description - break - i += 1 # Skip last post_description - - self.offset[container] = i - if desc: - # Only add code with description - self.codes.append(Code(description=desc, - block=node)) + + def visit_section(self, node:nodes.section) -> None: + self._section_level += 1 + + def depart_section(self, node:nodes.section) -> None: + self._section_level -= 1 + self._has_code_block = False + + # Skip section without content + if not self._secion_has_content(node): + return + # Skip toplevel section, we generate :class:`Document` for it + if self._section_level == 0: + return + + # TODO: code block + self.sections.append((Section(node) , node)) def unknown_visit(self, node:nodes.Node) -> None: pass # Ignore any unknown node - def unknown_departure(self, node:nodes.Node) -> None: pass # Ignore any unknown node - def is_description(self, node:nodes.Node) -> bool: - """Return whether given node can be description of code in :class:`Code` .""" - return isinstance(node, (nodes.Admonition, nodes.Sequential, - nodes.paragraph, nodes.field, nodes.option, - nodes.line_block)) - + ################## + # Helper methods # + ################## - def is_post_description(self, node:nodes.Node) -> bool: - """Return whether the given node can be post_description of code in :class:`Notes`.""" - return isinstance(node, nodes.Admonition) + def _secion_has_content(self, node:nodes.section) -> bool: + """ + A section has content when it has non-section child node + (except the title) + """ + num_subsection = len(list(node[0].traverse(nodes.section, + descend=False, + siblings=True))) + return num_subsection + 1 < len(node) From 228ee739cada94065fa6967610666346502efa14 Mon Sep 17 00:00:00 2001 From: Shengyu Zhang Date: Thu, 12 Aug 2021 23:13:44 +0800 Subject: [PATCH 2/7] Fix usage of snippet --- sphinxnotes/snippet/cli.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sphinxnotes/snippet/cli.py b/sphinxnotes/snippet/cli.py index cd33806..2fbb6a9 100644 --- a/sphinxnotes/snippet/cli.py +++ b/sphinxnotes/snippet/cli.py @@ -148,9 +148,9 @@ def _on_command_get(args:argparse.Namespace): print('no such index ID', file=sys.stderr) sys.exit(1) if args.text: - print('\n'.join(item.snippet.text())) + print('\n'.join(item.snippet.rst)) if args.file: - print(item.snippet.file()) + print(item.snippet.file) if args.url: # HACK: get doc id in better way doc_id, _ = args.cache.index_id_to_doc_id.get(index_id) @@ -159,13 +159,13 @@ def _on_command_get(args:argparse.Namespace): print(f'base URL for project {doc_id[0]} not configurated', file=sys.stderr) sys.exit(1) url = posixpath.join(base_url, doc_id[1] + '.html') - if item.snippet.refid(): - url += '#' + item.snippet.refid() + if item.snippet.refid: + url += '#' + item.snippet.refid print(url) if args.line_start: - print(item.snippet.scope()[0]) + print(item.snippet.lineno[0]) if args.line_end: - print(item.snippet.scope()[1]) + print(item.snippet.lineno[1]) def _on_command_integration(args:argparse.Namespace): From 86c557ce87bbff3c24fce5c853603c8ec81d9311 Mon Sep 17 00:00:00 2001 From: Shengyu Zhang Date: Fri, 13 Aug 2021 09:53:52 +0800 Subject: [PATCH 3/7] More debug log --- sphinxnotes/snippet/ext.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/sphinxnotes/snippet/ext.py b/sphinxnotes/snippet/ext.py index 494820a..fabb195 100644 --- a/sphinxnotes/snippet/ext.py +++ b/sphinxnotes/snippet/ext.py @@ -47,8 +47,10 @@ def extract_kinds(s:Snippet) -> str: def extract_excerpt(s:Snippet) -> str: - if isinstance(s, WithTitle) and s.title is not None: + if isinstance(s, Document) and s.title is not None: return '<' + s.title.text + '>' + elif isinstance(s, Section) and s.title is not None: + return '[' + s.title.text + ']' return '' @@ -83,7 +85,7 @@ def on_config_inited(app:Sphinx, appcfg:SphinxConfig) -> None: try: cache.load() except Exception as e: - logger.warning("failed to laod cache: %s" % e) + logger.warning("[snippet] failed to laod cache: %s" % e) def on_env_get_outdated(app:Sphinx, env:BuildEnvironment, added:Set[str], @@ -97,12 +99,13 @@ def on_env_get_outdated(app:Sphinx, env:BuildEnvironment, added:Set[str], def on_doctree_resolved(app:Sphinx, doctree:nodes.document, docname:str) -> None: if not isinstance(doctree, nodes.document): # XXX: It may caused by ablog - logger.debug('node %s is not nodes.document', type(doctree), location=doctree) + logger.debug('[snippet] node %s is not nodes.document', type(doctree), location=doctree) return pats = app.config.snippet_patterns doc = cache.setdefault((app.config.project, docname), []) - for s, n in pick(doctree): + snippets = pick(doctree) + for s, n in snippets: if not is_matched(pats, s, docname): continue doc.append(Item(snippet=s, @@ -116,6 +119,9 @@ def on_doctree_resolved(app:Sphinx, doctree:nodes.document, docname:str) -> None if len(doc) == 0: del cache[(app.config.project, docname)] + logger.debug('[snippet] picked %s/%s snippetes in %s', + len(doc), len(snippets), docname) + def on_builder_finished(app:Sphinx, exception) -> None: cache.dump() From 01201021fba66630cade81d7cdc0a5a4ac5f090b Mon Sep 17 00:00:00 2001 From: Shengyu Zhang Date: Fri, 13 Aug 2021 17:40:49 +0800 Subject: [PATCH 4/7] Rename kind -> tags --- sphinxnotes/snippet/cache.py | 6 +++--- sphinxnotes/snippet/cli.py | 12 ++++++------ sphinxnotes/snippet/ext.py | 16 ++++++++-------- sphinxnotes/snippet/table.py | 14 +++++++------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/sphinxnotes/snippet/cache.py b/sphinxnotes/snippet/cache.py index b1265a2..ca84c04 100644 --- a/sphinxnotes/snippet/cache.py +++ b/sphinxnotes/snippet/cache.py @@ -16,7 +16,7 @@ class Item(object): """ Item of snippet cache. """ snippet:Snippet - kinds:List[str] + tags:List[str] excerpt:str titlepath:List[str] keywords:List[str] @@ -24,7 +24,7 @@ class Item(object): DocID = Tuple[str,str] # (project, docname) IndexID = str # UUID -Index = Tuple[str,str,List[str],List[str]] # (kind, excerpt, titlepath, keywords) +Index = Tuple[str,str,List[str],List[str]] # (tags, excerpt, titlepath, keywords) class Cache(PDict): """A DocID -> List[Item] Cache.""" @@ -54,7 +54,7 @@ def post_dump(self, key:DocID, items:List[Item]) -> None: # Add new index to every where for i, item in enumerate(items): index_id = self.gen_index_id() - self.indexes[index_id] = (item.kinds, + self.indexes[index_id] = (item.tags, item.excerpt, item.titlepath, item.keywords) diff --git a/sphinxnotes/snippet/cli.py b/sphinxnotes/snippet/cli.py index 2fbb6a9..22c192c 100644 --- a/sphinxnotes/snippet/cli.py +++ b/sphinxnotes/snippet/cli.py @@ -44,10 +44,10 @@ def main(argv:List[str]=sys.argv[1:]) -> int: parser = argparse.ArgumentParser(prog=__name__, description=__description__, formatter_class=HelpFormatter, epilog=dedent(""" - snippet kinds: - d (document) a complete reST document + snippet tags: + d (document) a reST document s (section) a reST section - c (code) a snippet with code blocks + c (code) snippet with code blocks * (any) wildcard for any snippet""")) parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + __version__) parser.add_argument('-c', '--config', default=DEFAULT_CONFIG_FILE, help='path to configuration file') @@ -62,8 +62,8 @@ def main(argv:List[str]=sys.argv[1:]) -> int: listparser = subparsers.add_parser('list', aliases=['l'], formatter_class=HelpFormatter, help='list snippet indexes, columns of indexes: %s' % COLUMNS) - listparser.add_argument('--kinds', '-k', type=str, default='*', - help='list specified kinds only') + listparser.add_argument('--tags', '-t', type=str, default='*', + help='list specified tags only') listparser.add_argument('--width', '-w', type=int, default=get_terminal_size((120, 0)).columns, help='width in characters of output') @@ -136,7 +136,7 @@ def _on_command_stat(args:argparse.Namespace): def _on_command_list(args:argparse.Namespace): - rows = tablify(args.cache.indexes, args.kinds, args.width) + rows = tablify(args.cache.indexes, args.tags, args.width) for row in rows: print(row) diff --git a/sphinxnotes/snippet/ext.py b/sphinxnotes/snippet/ext.py index fabb195..3848915 100644 --- a/sphinxnotes/snippet/ext.py +++ b/sphinxnotes/snippet/ext.py @@ -35,15 +35,15 @@ extractor:Extractor = Extractor() -def extract_kinds(s:Snippet) -> str: - kinds = '' +def extract_tags(s:Snippet) -> str: + tags = '' if isinstance(s, Document): - kinds += 'd' + tags += 'd' elif isinstance(s, Section): - kinds += 's' + tags += 's' if isinstance(s, WithCodeBlock): - kinds += 'c' - return kinds + tags += 'c' + return tags def extract_excerpt(s:Snippet) -> str: @@ -68,7 +68,7 @@ def is_matched(pats:Dict[str,List[str]], s:[Snippet], docname:str) -> bool: for pat in pats['*']: if re.match(pat, docname): return True - for k in extract_kinds(s): + for k in extract_tags(s): if k not in pats: continue for pat in pats[k]: @@ -109,7 +109,7 @@ def on_doctree_resolved(app:Sphinx, doctree:nodes.document, docname:str) -> None if not is_matched(pats, s, docname): continue doc.append(Item(snippet=s, - kinds=extract_kinds(s), + tags=extract_tags(s), excerpt=extract_excerpt(s), keywords=extract_keywords(s), titlepath=resolve_fullpath(app.env, diff --git a/sphinxnotes/snippet/table.py b/sphinxnotes/snippet/table.py index e51e25b..beaf3cd 100644 --- a/sphinxnotes/snippet/table.py +++ b/sphinxnotes/snippet/table.py @@ -12,17 +12,17 @@ from .cache import Index, IndexID from .utils import ellipsis -COLUMNS = ['id', 'kind', 'excerpt', 'path', 'keywords'] +COLUMNS = ['id', 'tags', 'excerpt', 'path', 'keywords'] VISIABLE_COLUMNS = COLUMNS[1:4] COLUMN_DELIMITER = ' ' -def tablify(indexes: Dict[IndexID,Index], kinds:str, width:int) -> Iterator[str]: +def tablify(indexes: Dict[IndexID,Index], tags:str, width:int) -> Iterator[str]: """ Create a table from sequence of cache.Index. """ # Calcuate width width = width - kind_width = len(VISIABLE_COLUMNS[0]) - width -= kind_width + tags_width = len(VISIABLE_COLUMNS[0]) + width -= tags_width excerpt_width = max(int(width * 6/10), len(VISIABLE_COLUMNS[1])) path_width = max(int(width * 4/10), len(VISIABLE_COLUMNS[2])) path_comp_width = path_width // 3 @@ -30,7 +30,7 @@ def tablify(indexes: Dict[IndexID,Index], kinds:str, width:int) -> Iterator[str] # Write header header = COLUMN_DELIMITER.join( [COLUMNS[0].upper(), - ellipsis.ellipsis(COLUMNS[1].upper(), kind_width, blank_sym=' '), + ellipsis.ellipsis(COLUMNS[1].upper(), tags_width, blank_sym=' '), ellipsis.ellipsis(COLUMNS[2].upper(), excerpt_width, blank_sym=' '), ellipsis.ellipsis(COLUMNS[3].upper(), path_width, blank_sym=' ' ), COLUMNS[4].upper()]) @@ -39,11 +39,11 @@ def tablify(indexes: Dict[IndexID,Index], kinds:str, width:int) -> Iterator[str] # Write rows for index_id, index in indexes.items(): # TODO: assert index? - if index[0] not in kinds and '*' not in kinds: + if index[0] not in tags and '*' not in tags: continue row = COLUMN_DELIMITER.join( [index_id, # ID - ellipsis.ellipsis(f'[{index[0]}]', kind_width, blank_sym=' '), # Kind + ellipsis.ellipsis(f'[{index[0]}]', tags_width, blank_sym=' '), # Kind ellipsis.ellipsis(index[1], excerpt_width, blank_sym=' '), # Excerpt ellipsis.join(index[2], path_width, path_comp_width, blank_sym=' ' ), # Titleppath ','.join(index[3])]) # Keywords From f5916bd5ed8523a302cba333529873191ad1e113 Mon Sep 17 00:00:00 2001 From: Shengyu Zhang Date: Sat, 14 Aug 2021 13:39:23 +0800 Subject: [PATCH 5/7] integration: Refactor - Move more stuff from plugin to binding - Drop snippet view function - Hardcode vim as editor, xdg-open as url viewer - Add bash binding support --- sphinxnotes/snippet/cli.py | 15 ++-- sphinxnotes/snippet/integration/binding.sh | 41 +++++++++++ sphinxnotes/snippet/integration/binding.vim | 31 +++++++- sphinxnotes/snippet/integration/binding.zsh | 28 ++++++-- sphinxnotes/snippet/integration/plugin.sh | 76 ++------------------ sphinxnotes/snippet/integration/plugin.vim | 78 ++------------------- sphinxnotes/snippet/integration/plugin.zsh | 28 -------- 7 files changed, 115 insertions(+), 182 deletions(-) create mode 100644 sphinxnotes/snippet/integration/binding.sh delete mode 100644 sphinxnotes/snippet/integration/plugin.zsh diff --git a/sphinxnotes/snippet/cli.py b/sphinxnotes/snippet/cli.py index 22c192c..6b3c822 100644 --- a/sphinxnotes/snippet/cli.py +++ b/sphinxnotes/snippet/cli.py @@ -88,10 +88,11 @@ def main(argv:List[str]=sys.argv[1:]) -> int: igparser = subparsers.add_parser('integration', aliases=['i'], formatter_class=HelpFormatter, help='integration related commands') - igparser.add_argument('--sh', '-s', action='store_true', help='dump POSIX shell integration script') + igparser.add_argument('--sh', '-s', action='store_true', help='dump bash shell integration script') + igparser.add_argument('--sh-binding', action='store_true', help='dump recommended bash key binding') igparser.add_argument('--zsh', '-z', action='store_true', help='dump zsh integration script') igparser.add_argument('--zsh-binding', action='store_true', help='dump recommended zsh key binding') - igparser.add_argument('--vim', '-v', action='store_true', help='dump (neo)vim integration script (NOTE: for now, only neovim is supported)') + igparser.add_argument('--vim', '-v', action='store_true', help='dump (neo)vim integration script') igparser.add_argument('--vim-binding', action='store_true', help='dump recommended (neo)vim key binding') igparser.set_defaults(func=_on_command_integration, parser=igparser) @@ -172,13 +173,17 @@ def _on_command_integration(args:argparse.Namespace): if args.sh: with open(get_integration_file('plugin.sh'), 'r') as f: print(f.read()) + if args.sh_binding: + with open(get_integration_file('binding.sh'), 'r') as f: + print(f.read()) if args.zsh: - # Zsh plugin depends on POSIX shell plugin + # Zsh plugin depends on Bash shell plugin with open(get_integration_file('plugin.sh'), 'r') as f: print(f.read()) - with open(get_integration_file('plugin.zsh'), 'r') as f: - print(f.read()) if args.zsh_binding: + # Zsh binding depends on Bash shell binding + with open(get_integration_file('binding.sh'), 'r') as f: + print(f.read()) with open(get_integration_file('binding.zsh'), 'r') as f: print(f.read()) if args.vim: diff --git a/sphinxnotes/snippet/integration/binding.sh b/sphinxnotes/snippet/integration/binding.sh new file mode 100644 index 0000000..7b5be10 --- /dev/null +++ b/sphinxnotes/snippet/integration/binding.sh @@ -0,0 +1,41 @@ +# Bash Shell key binding for sphinxnotes-snippet +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# :Author: Shengyu Zhang +# :Date: 2021-08-114 +# :Version: 20210814 +# +# .. note:: Must source :file:`./plugin.sh` to get `snippet_list` functions. + +function snippet_edit() { + selection=$(snippet_list ds) + [ -z "$selection" ] && return + + echo "vim +\$($SNIPPET get --line-start $selection) \$($SNIPPET get --file $selection)" +} + +function snippet_url() { + selection=$(snippet_list ds) + [ -z "$selection" ] && return + + echo "xdg-open \$($SNIPPET get --url $selection)" +} + +function snippet_sh_bind_wrapper() { + READLINE_LINE="$($1)" + READLINE_POINT=${#READLINE_LINE} +} + +function snippet_sh_do_bind() { + bind '"\XXacceptline": accept-line' + bind -x '"\XXsnippetedit": snippet_sh_bind_wrapper snippet_edit' + bind -x '"\XXsnippeturl": snippet_sh_bind_wrapper snippet_url' + bind '"\C-ke": "\XXsnippetedit\XXacceptline"' + bind '"\C-ku": "\XXsnippeturl\XXacceptline"' +} + +# Bind key if bind command exists +# (the script may sourced by Zsh) +command -v bind 2>&1 1>/dev/null && snippet_sh_do_bind + +# vim: set shiftwidth=2: diff --git a/sphinxnotes/snippet/integration/binding.vim b/sphinxnotes/snippet/integration/binding.vim index 4cad89e..bb8d30e 100644 --- a/sphinxnotes/snippet/integration/binding.vim +++ b/sphinxnotes/snippet/integration/binding.vim @@ -3,9 +3,36 @@ " " :Author: Shengyu Zhang " :Date: 2021-04-12 -" :Version: 20210412 +" :Version: 20210814 " -nmap v :call g:SphinxNotesSnippetListAndView() +function! g:SphinxNotesSnippetEdit(id) + let file = system(join([s:snippet, 'get', '--file', a:id], ' ')) + let line = system(join([s:snippet, 'get', '--line-start', a:id], ' ')) + execute 'tabedit ' . file + execute line +endfunction + +function! g:SphinxNotesSnippetListAndEdit() + function! s:CallEdit(selection) + call g:SphinxNotesSnippetEdit(s:SplitID(a:selection)) + endfunction + call g:SphinxNotesSnippetList(function('s:CallEdit'), 'ds') +endfunction + +function! g:SphinxNotesSnippetUrl(id) + let url_list = systemlist(join([s:snippet, 'get', '--url', a:id], ' ')) + for url in url_list + echo system('xdg-open ' . shellescape(url)) + endfor +endfunction + +function! g:SphinxNotesSnippetListAndUrl() + function! s:CallUrl(selection) + call g:SphinxNotesSnippetUrl(s:SplitID(a:selection)) + endfunction + call g:SphinxNotesSnippetList(function('s:CallUrl'), 'ds') +endfunction + nmap e :call g:SphinxNotesSnippetListAndEdit() nmap u :call g:SphinxNotesSnippetListAndUrl() diff --git a/sphinxnotes/snippet/integration/binding.zsh b/sphinxnotes/snippet/integration/binding.zsh index 74751bd..a8c5174 100644 --- a/sphinxnotes/snippet/integration/binding.zsh +++ b/sphinxnotes/snippet/integration/binding.zsh @@ -3,10 +3,30 @@ # # :Author: Shengyu Zhang # :Date: 2021-04-12 -# :Version: 20210412 +# :Version: 20210814 -bindkey '^kv' snippet-view -bindkey '^ke' snippet-edit -bindkey '^ku' snippet-url +# $1: One of snippet_* functions +function snippet_z_bind_wrapper() { + cmd=$($1) + if [ ! -z "$cmd" ]; then + BUFFER="$cmd" + zle accept-line + fi +} + +function snippet_z_edit() { + snippet_z_bind_wrapper snippet_edit +} + +function snippet_z_url() { + snippet_z_bind_wrapper snippet_url +} + +# Define widgets +zle -N snippet_z_edit +zle -N snippet_z_url + +bindkey '^ke' snippet_z_edit +bindkey '^ku' snippet_z_url # vim: set shiftwidth=2: diff --git a/sphinxnotes/snippet/integration/plugin.sh b/sphinxnotes/snippet/integration/plugin.sh index 82af47f..e91bb2e 100644 --- a/sphinxnotes/snippet/integration/plugin.sh +++ b/sphinxnotes/snippet/integration/plugin.sh @@ -1,83 +1,19 @@ -# POISX Shell integration for sphinxnotes-snippet +# Bash Shell integration for sphinxnotes-snippet # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # :Author: Shengyu Zhang # :Date: 2021-03-20 -# :Version: 20210420 +# :Version: 20210814 # Make sure we have $SNIPPET [ -z "$SNIPPET"] && SNIPPET='snippet' -# $1: kinds +# Arguments: $1: kinds +# Returns: snippet_id function snippet_list() { - $SNIPPET list --kinds $1 --width $(($(tput cols) - 2))| \ - fzf --with-nth 2.. --no-hscroll --header-lines 1 | \ + $SNIPPET list --tags $1 --width $(($(tput cols) - 2)) | \ + fzf --with-nth 2.. --no-hscroll --header-lines 1 | \ cut -d ' ' -f1 } -function snippet_view() { - index_id=$(snippet_list ds) - [ -z "$index_id" ] && return - - # Make sure we have $PAGER - if [ -z "$PAGER" ]; then - if [ ! -z "$(where bat)" ]; then - PAGER='bat --language rst --italic-text always --style plain --paging always' - elif [ ! -z "$(where less)" ]; then - PAGER='less' - elif [ ! -z "$(where more)" ]; then - PAGER='more' - else - echo "No pager available!" >&2 - return - fi - fi - - echo "$SNIPPET get --text $index_id | $PAGER" -} - -function snippet_edit() { - index_id=$(snippet_list ds) - [ -z "$index_id" ] && return - - # Make sure we have $EDITOR - if [ -z "$EDITOR" ]; then - if [ ! -z "$(where vim)" ]; then - EDITOR='vim' - elif [ ! -z "$(where nvim)" ]; then - EDITOR='nvim' - elif [ ! -z "$(where nano)" ]; then - EDITOR='nano' - else - echo "No editor available!" >&2 - return - fi - fi - - echo "$EDITOR \$($SNIPPET get --file $index_id)" -} - -function snippet_url() { - index_id=$(snippet_list ds) - [ -z "$index_id" ] && return - - # Make sure we have $BROWSER - if [ -z "$BROWSER" ]; then - if [ ! -z "$(where firefox)" ]; then - BROWSER='firefox' - elif [ ! -z "$(where chromium)" ]; then - BROWSER='chromium' - elif [ ! -z "$(where chrome)" ]; then - BROWSER='chrome' - elif [ ! -z "$(where xdg-open)" ]; then - BROWSER='xdg-open' - else - echo "No browser available!" >&2 - return - fi - fi - - echo "$BROWSER \$($SNIPPET get --url $index_id)" -} - # vim: set shiftwidth=2: diff --git a/sphinxnotes/snippet/integration/plugin.vim b/sphinxnotes/snippet/integration/plugin.vim index ad4362a..603df46 100644 --- a/sphinxnotes/snippet/integration/plugin.vim +++ b/sphinxnotes/snippet/integration/plugin.vim @@ -1,9 +1,9 @@ -" NeoVim integration for sphinxnotes-snippet -" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +" Vim integration for sphinxnotes-snippet +" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ " " :Author: Shengyu Zhang " :Date: 2021-04-01 -" :Version: 20210413 +" :Version: 20210814 " " NOTE: junegunn/fzf.vim is required @@ -13,9 +13,9 @@ function! s:SplitID(row) return split(a:row, ' ')[0] endfunction -function! g:SphinxNotesSnippetList(callback, kinds) +function! g:SphinxNotesSnippetList(callback, tags) let cmd = [s:snippet, 'list', - \ '--kinds', a:kinds, + \ '--tags', a:tags, \ '--width', &columns - 2, \ ] call fzf#run({ @@ -25,72 +25,4 @@ function! g:SphinxNotesSnippetList(callback, kinds) \ }) endfunction -function! g:SphinxNotesSnippetListAndView() - function! s:CallView(selection) - call g:SphinxNotesSnippetView(s:SplitID(a:selection)) - endfunction - call g:SphinxNotesSnippetList(function('s:CallView'), 'ds') -endfunction - -" https://github.com/anhmv/vim-float-window/blob/master/plugin/float-window.vim -function! g:SphinxNotesSnippetView(id) - let height = float2nr((&lines - 2) / 1.5) - let row = float2nr((&lines - height) / 2) - let width = float2nr(&columns / 1.5) - let col = float2nr((&columns - width) / 2) - - " Main Window - let opts = { - \ 'relative': 'editor', - \ 'style': 'minimal', - \ 'width': width, - \ 'height': height, - \ 'col': col, - \ 'row': row, - \ } - - let buf = nvim_create_buf(v:false, v:true) - " Global for :call - let g:sphinx_notes_snippet_win = nvim_open_win(buf, v:true, opts) - - " The content is always reStructuredText for now - set filetype=rst - " Press enter to return - nmap :call nvim_win_close(g:sphinx_notes_snippet_win, v:true) - - let cmd = [s:snippet, 'get', '--text', a:id] - execute '$read !' . join(cmd, ' ') - execute '$read !' . '..' - call append(line('$'), [ - \ '.. Inserted By sphinxnotes-snippet:', - \ '', - \ ' Press to return']) -endfunction - -function! g:SphinxNotesSnippetEdit(id) - let cmd = [s:snippet, 'get', '--file', a:id] - execute '$tabedit ' . system(join(cmd, ' ')) -endfunction - -function! g:SphinxNotesSnippetListAndEdit() - function! s:CallEdit(selection) - call g:SphinxNotesSnippetEdit(s:SplitID(a:selection)) - endfunction - call g:SphinxNotesSnippetList(function('s:CallEdit'), 'ds') -endfunction - -function! g:SphinxNotesSnippetUrl(id) - let cmd = [s:snippet, 'get', '--url', a:id] - for url in systemlist(join(cmd, ' ')) - echo system('xdg-open ' . shellescape(url)) - endfor -endfunction - -function! g:SphinxNotesSnippetListAndUrl() - function! s:CallUrl(selection) - call g:SphinxNotesSnippetUrl(s:SplitID(a:selection)) - endfunction - call g:SphinxNotesSnippetList(function('s:CallUrl'), 'ds') -endfunction - " vim: set shiftwidth=2: diff --git a/sphinxnotes/snippet/integration/plugin.zsh b/sphinxnotes/snippet/integration/plugin.zsh deleted file mode 100644 index 3a2a182..0000000 --- a/sphinxnotes/snippet/integration/plugin.zsh +++ /dev/null @@ -1,28 +0,0 @@ -# Z Shell integration for sphinxnotes-snippet -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# -# :Author: Shengyu Zhang -# :Date: 2021-03-20 -# :Version: 20210420 -# -# NOTE: Must source :file:`./plugin.sh` to get snippet_* functions. - -# $1: One of snippet_* functions -function z-wrapper() { - cmd=$($1) - if [ ! -z "$cmd" ]; then - BUFFER="$cmd" - zle accept-line - fi -} - -function snippet-view(){ z-wrapper snippet_view } -function snippet-edit(){ z-wrapper snippet_edit } -function snippet-url(){ z-wrapper snippet_url } - -# Define a widget -zle -N snippet-view -zle -N snippet-edit -zle -N snippet-url - -# vim: set shiftwidth=2: From d25ea30833ca65fd6b35dcc8dbaf6afc6bcadcae Mon Sep 17 00:00:00 2001 From: Shengyu Zhang Date: Sat, 14 Aug 2021 13:52:42 +0800 Subject: [PATCH 6/7] Minor changes --- sphinxnotes/__init__.py | 3 +++ sphinxnotes/snippet/cli.py | 1 - sphinxnotes/snippet/ext.py | 10 ++++------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sphinxnotes/__init__.py b/sphinxnotes/__init__.py index ad6a151..91b7877 100644 --- a/sphinxnotes/__init__.py +++ b/sphinxnotes/__init__.py @@ -8,3 +8,6 @@ :copyright: Copyright 2020 by the Shengyu Zhang. """ + +# NOTE: Don't use ``__import__('pkg_resources').declare_namespace(__name__)`` +# here, it causes the application startup process to be slower diff --git a/sphinxnotes/snippet/cli.py b/sphinxnotes/snippet/cli.py index 6b3c822..21ceae2 100644 --- a/sphinxnotes/snippet/cli.py +++ b/sphinxnotes/snippet/cli.py @@ -96,7 +96,6 @@ def main(argv:List[str]=sys.argv[1:]) -> int: igparser.add_argument('--vim-binding', action='store_true', help='dump recommended (neo)vim key binding') igparser.set_defaults(func=_on_command_integration, parser=igparser) - # Parse command line arguments args = parser.parse_args(argv) diff --git a/sphinxnotes/snippet/ext.py b/sphinxnotes/snippet/ext.py index 3848915..0391ad0 100644 --- a/sphinxnotes/snippet/ext.py +++ b/sphinxnotes/snippet/ext.py @@ -9,7 +9,7 @@ """ from __future__ import annotations -from typing import List, Set, TYPE_CHECKING, Type, Dict +from typing import List, Set, TYPE_CHECKING, Dict import re from docutils import nodes @@ -21,7 +21,7 @@ from sphinx.util import logging from .config import Config -from . import Snippet, WithTitle, Document, Section, WithCodeBlock +from . import Snippet, WithTitle, Document, Section from .picker import pick from .cache import Cache, Item from .keyword import Extractor @@ -41,8 +41,6 @@ def extract_tags(s:Snippet) -> str: tags += 'd' elif isinstance(s, Section): tags += 's' - if isinstance(s, WithCodeBlock): - tags += 'c' return tags @@ -63,8 +61,8 @@ def extract_keywords(s:Snippet) -> List[str]: def is_matched(pats:Dict[str,List[str]], s:[Snippet], docname:str) -> bool: - # Wildcard - if '*' in pats: + """Whether the snippet's tags and docname matched by given patterns pats""" + if '*' in pats: # Wildcard for pat in pats['*']: if re.match(pat, docname): return True From dbdbdeacd50751c3e2509e45aeb47f74153bafb9 Mon Sep 17 00:00:00 2001 From: Shengyu Zhang Date: Sat, 14 Aug 2021 14:46:30 +0800 Subject: [PATCH 7/7] Refine titlepath - Drop "subtitle" support - Retrurn list of nodes.title but not string - include_project is not need --- sphinxnotes/snippet/ext.py | 7 +- sphinxnotes/snippet/utils/titlepath.py | 97 +++++++------------------- 2 files changed, 26 insertions(+), 78 deletions(-) diff --git a/sphinxnotes/snippet/ext.py b/sphinxnotes/snippet/ext.py index 0391ad0..c706709 100644 --- a/sphinxnotes/snippet/ext.py +++ b/sphinxnotes/snippet/ext.py @@ -25,7 +25,7 @@ from .picker import pick from .cache import Cache, Item from .keyword import Extractor -from .utils.titlepath import resolve_fullpath +from .utils import titlepath from .builder import Builder @@ -110,10 +110,7 @@ def on_doctree_resolved(app:Sphinx, doctree:nodes.document, docname:str) -> None tags=extract_tags(s), excerpt=extract_excerpt(s), keywords=extract_keywords(s), - titlepath=resolve_fullpath(app.env, - docname, - n, - include_project=True))) + titlepath=[x.astext() for x in titlepath.resolve(app.env, docname, n)])) if len(doc) == 0: del cache[(app.config.project, docname)] diff --git a/sphinxnotes/snippet/utils/titlepath.py b/sphinxnotes/snippet/utils/titlepath.py index 847141a..a00c3bd 100644 --- a/sphinxnotes/snippet/utils/titlepath.py +++ b/sphinxnotes/snippet/utils/titlepath.py @@ -8,7 +8,7 @@ :license: BSD, see LICENSE for details. """ from __future__ import annotations -from typing import List, Optional, Tuple, TYPE_CHECKING +from typing import List, TYPE_CHECKING from docutils import nodes @@ -16,95 +16,46 @@ from sphinx.enviornment import BuilderEnviornment -def _safe_descend(node:nodes.Node, *args: int) -> Optional[nodes.Node]: - """Get node descend in a safe way.""" - try: - for index in args: - node = node[index] - return node - except: - return None +def resolve(env: BuilderEnviornment, docname:str, node:nodes.Node) -> List[nodes.title]: + return resolve_section(node) + resolve_document(env, docname) -def resolve_fullpath(env: BuilderEnviornment, docname:str, node:nodes.Node, - include_project:bool=False) -> List[str]: - return [x.astext() for x in resolve_sectpath(node.document, node)] + \ - resolve_docpath(env, docname, include_project=include_project) - - -def resolve_sectpath(doctree:nodes.document, node:nodes.Node) -> List[nodes.title]: +def resolve_section(node:nodes.section) -> List[nodes.title]: # FIXME: doc is None - _, subtitlenode = resolve_doctitle(doctree) titlenodes = [] while node: - secttitle = resolve_secttitle(node) + if len(node) > 0 and isinstance(node[0], nodes.title): + titlenodes.append(node[0]) node = node.parent - if not secttitle or secttitle == subtitlenode: - continue - titlenodes.append(secttitle) return titlenodes -def resolve_secttitle(node:nodes.Node) -> Optional[nodes.title]: - titlenode = _safe_descend(node.parent, 0) - if not isinstance(titlenode, nodes.title): - return None - return titlenode - - -def resolve_docpath(env:BuilderEnviornment, docname:str, include_project:bool=False) -> List[str]: +def resolve_document(env:BuilderEnviornment, docname:str) -> List[nodes.title]: + """ + .. note:: Title of document itself does not included in the returned list + """ titles = [] - - if include_project: - titles.append(env.config.project) - master_doc = env.config.master_doc v = docname.split('/') - if v.pop() == master_doc: - if v: - # If docname is "a/b/index", we need titles of "a" - v.pop() - else: - # docname is "index", no need to get docpath, it is root doc - return [] + + # Exclude self + if v.pop() == master_doc and v: + # If self is master_doc, like: "a/b/c/index", we only return titles + # of "a/b/", so pop again + v.pop() + + # Collect master doc title in docname while v: master_docname = '/'.join(v + [master_doc]) if master_docname in env.titles: - title = env.titles[master_docname].astext() + title = env.titles[master_docname] else: - title = v[-1].title() + title = nodes.title(text=v[-1].title()) # FIXME: Create mock title for now titles.append(title) v.pop() - return titles[::-1] # Reverse inplace - - -def resolve_doctitle(doctree:nodes.document) -> Tuple[Optional[nodes.title], - Optional[nodes.title]]: - - toplevel_sectnode = doctree.next_node(nodes.section) - if not toplevel_sectnode: - return (None, None) + # Include title of top-level master doc + if master_doc in env.titles: + titles.append(env.titles[master_doc]) - titlenode = _safe_descend(toplevel_sectnode, 0) - # NOTE: nodes.subtitle does not make senses beacuse Sphinx doesn't support - # subtitle: - # - # > Sphinx does not support a "subtitle". - # > Sphinx recognizes it as a mere second level section - # - # ref: - # - https://github.com/sphinx-doc/sphinx/issues/3574#issuecomment-288722585 - # - https://github.com/sphinx-doc/sphinx/issues/3567#issuecomment-288093991 - if len(toplevel_sectnode) != 2: - return (titlenode, None) - # HACK: For our convenience, we regard second level section title - # (under document) as subtitle:: - #
- # - # <section> - # <(sub)title> - subtitlenode = toplevel_sectnode[1][0] - if not isinstance(subtitlenode, nodes.title): - return (titlenode, None) - return (titlenode, subtitlenode) + return titles