Skip to content

Commit deef7e4

Browse files
authored
Impl code snippet (#37)
And various refactoring and bug fixes.
1 parent 021df50 commit deef7e4

File tree

8 files changed

+173
-141
lines changed

8 files changed

+173
-141
lines changed

src/sphinxnotes/snippet/cache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def post_purge(self, key: DocID, value: list[Item]) -> None:
9393
def get_by_index_id(self, key: IndexID) -> Item | None:
9494
"""Like get(), but use IndexID as key."""
9595
doc_id, item_index = self.index_id_to_doc_id.get(key, (None, None))
96-
if not doc_id or not item_index:
96+
if not doc_id or item_index is None:
9797
return None
9898
return self[doc_id][item_index]
9999

@@ -105,4 +105,4 @@ def gen_index_id(self) -> str:
105105

106106
def stringify(self, key: DocID, value: list[Item]) -> str:
107107
"""Overwrite PDict.stringify."""
108-
return key[1]
108+
return key[1] # docname

src/sphinxnotes/snippet/cli.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ def main(argv: list[str] = sys.argv[1:]):
6060
formatter_class=HelpFormatter,
6161
epilog=dedent("""
6262
snippet tags:
63-
d (document) a reST document
64-
s (section) a reST section
65-
c (code) snippet with code blocks
63+
d (document) a document
64+
s (section) a section
65+
c (code) a code block
6666
* (any) wildcard for any snippet"""),
6767
)
6868
parser.add_argument(
@@ -140,7 +140,12 @@ def main(argv: list[str] = sys.argv[1:]):
140140
'--text',
141141
'-t',
142142
action='store_true',
143-
help='get source reStructuredText of snippet',
143+
help='get text representation of snippet',
144+
)
145+
getparser.add_argument(
146+
'--src',
147+
action='store_true',
148+
help='get source text of snippet',
144149
)
145150
getparser.add_argument(
146151
'--url',
@@ -273,7 +278,9 @@ def p(*args, **opts):
273278
p('no such index ID', file=sys.stderr)
274279
sys.exit(1)
275280
if args.text:
276-
p('\n'.join(item.snippet.rst))
281+
p('\n'.join(item.snippet.text))
282+
if args.src:
283+
p('\n'.join(item.snippet.source))
277284
if args.docname:
278285
p(item.snippet.docname)
279286
if args.file:

src/sphinxnotes/snippet/ext.py

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from collections.abc import Iterator
2727

2828
from .config import Config
29-
from .snippets import Snippet, WithTitle, Document, Section
29+
from .snippets import Snippet, WithTitle, Document, Section, Code
3030
from .picker import pick
3131
from .cache import Cache, Item
3232
from .keyword import Extractor
@@ -45,53 +45,38 @@ def extract_tags(s: Snippet) -> str:
4545
tags += 'd'
4646
elif isinstance(s, Section):
4747
tags += 's'
48+
elif isinstance(s, Code):
49+
tags += 'c'
4850
return tags
4951

5052

5153
def extract_excerpt(s: Snippet) -> str:
5254
if isinstance(s, Document) and s.title is not None:
53-
return '<' + s.title.text + '>'
55+
return '<' + s.title + '>'
5456
elif isinstance(s, Section) and s.title is not None:
55-
return '[' + s.title.text + ']'
57+
return '[' + s.title + ']'
58+
elif isinstance(s, Code):
59+
return '`' + (s.lang + ':').ljust(8, ' ') + ' ' + s.desc + '`'
5660
return ''
5761

5862

5963
def extract_keywords(s: Snippet) -> list[str]:
6064
keywords = [s.docname]
61-
# TODO: Deal with more snippet
6265
if isinstance(s, WithTitle) and s.title is not None:
63-
keywords.extend(extractor.extract(s.title.text, strip_stopwords=False))
66+
keywords.extend(extractor.extract(s.title, strip_stopwords=False))
67+
if isinstance(s, Code):
68+
keywords.extend(extractor.extract(s.desc, strip_stopwords=False))
6469
return keywords
6570

6671

67-
def is_document_matched(
68-
pats: dict[str, list[str]], docname: str
69-
) -> dict[str, list[str]]:
70-
"""Whether the docname matched by given patterns pats"""
71-
new_pats = {}
72-
for tag, ps in pats.items():
72+
def _get_document_allowed_tags(pats: dict[str, list[str]], docname: str) -> str:
73+
"""Return the tags of snippets that are allowed to be picked from the document."""
74+
allowed_tags = ''
75+
for tags, ps in pats.items():
7376
for pat in ps:
7477
if re.match(pat, docname):
75-
new_pats.setdefault(tag, []).append(pat)
76-
return new_pats
77-
78-
79-
def is_snippet_matched(pats: dict[str, list[str]], s: [Snippet], docname: str) -> bool:
80-
"""Whether the snippet's tags and docname matched by given patterns pats"""
81-
if '*' in pats: # Wildcard
82-
for pat in pats['*']:
83-
if re.match(pat, docname):
84-
return True
85-
86-
not_in_pats = True
87-
for k in extract_tags(s):
88-
if k not in pats:
89-
continue
90-
not_in_pats = False
91-
for pat in pats[k]:
92-
if re.match(pat, docname):
93-
return True
94-
return not_in_pats
78+
allowed_tags += tags
79+
return allowed_tags
9580

9681

9782
def on_config_inited(app: Sphinx, appcfg: SphinxConfig) -> None:
@@ -113,6 +98,7 @@ def on_env_get_outdated(
11398
removed: set[str],
11499
) -> list[str]:
115100
# Remove purged indexes and snippetes from db
101+
assert cache is not None
116102
for docname in removed:
117103
del cache[(app.config.project, docname)]
118104
return []
@@ -126,15 +112,16 @@ def on_doctree_resolved(app: Sphinx, doctree: nodes.document, docname: str) -> N
126112
)
127113
return
128114

129-
pats = is_document_matched(app.config.snippet_patterns, docname)
130-
if len(pats) == 0:
131-
logger.debug('[snippet] skip picking because %s is not matched', docname)
115+
allowed_tags = _get_document_allowed_tags(app.config.snippet_patterns, docname)
116+
if not allowed_tags:
117+
logger.debug('[snippet] skip picking: no tag allowed for document %s', docname)
132118
return
133119

134120
doc = []
135121
snippets = pick(app, doctree, docname)
136122
for s, n in snippets:
137-
if not is_snippet_matched(pats, s, docname):
123+
# FIXME: Better filter logic.
124+
if extract_tags(s) not in allowed_tags:
138125
continue
139126
tpath = [x.astext() for x in titlepath.resolve(app.env, docname, n)]
140127
if isinstance(s, Section):
@@ -162,6 +149,7 @@ def on_doctree_resolved(app: Sphinx, doctree: nodes.document, docname: str) -> N
162149

163150

164151
def on_builder_finished(app: Sphinx, exception) -> None:
152+
assert cache is not None
165153
cache.dump()
166154

167155

src/sphinxnotes/snippet/integration/binding.nvim

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function! g:SphinxNotesSnippetListAndView()
1111
function! s:CallView(selection)
1212
call g:SphinxNotesSnippetView(s:SplitID(a:selection))
1313
endfunction
14-
call g:SphinxNotesSnippetList(function('s:CallView'), 'ds')
14+
call g:SphinxNotesSnippetList(function('s:CallView'), '*')
1515
endfunction
1616

1717
" https://github.com/anhmv/vim-float-window/blob/master/plugin/float-window.vim
@@ -40,7 +40,7 @@ function! g:SphinxNotesSnippetView(id)
4040
" Press enter to return
4141
nmap <buffer> <CR> :call nvim_win_close(g:sphinx_notes_snippet_win, v:true)<CR>
4242
43-
let cmd = [s:snippet, 'get', '--text', a:id]
43+
let cmd = [s:snippet, 'get', '--src', a:id]
4444
call append(line('$'), ['.. hint:: Press <ENTER> to return'])
4545
execute '$read !' . '..'
4646
execute '$read !' . join(cmd, ' ')

src/sphinxnotes/snippet/integration/binding.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# :Version: 20240828
77

88
function snippet_view() {
9-
selection=$(snippet_list --tags ds)
9+
selection=$(snippet_list)
1010
[ -z "$selection" ] && return
1111

1212
# Make sure we have $PAGER
@@ -18,7 +18,7 @@ function snippet_view() {
1818
fi
1919
fi
2020

21-
echo "$SNIPPET get --text $selection | $PAGER"
21+
echo "$SNIPPET get --src $selection | $PAGER"
2222
}
2323

2424
function snippet_edit() {

src/sphinxnotes/snippet/picker.py

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from sphinx.util import logging
1717

18-
from .snippets import Snippet, Section, Document
18+
from .snippets import Snippet, Section, Document, Code
1919

2020
if TYPE_CHECKING:
2121
from sphinx.application import Sphinx
@@ -25,81 +25,71 @@
2525

2626
def pick(
2727
app: Sphinx, doctree: nodes.document, docname: str
28-
) -> list[tuple[Snippet, nodes.section]]:
28+
) -> list[tuple[Snippet, nodes.Element]]:
2929
"""
30-
Pick snippets from document, return a list of snippet and the section
31-
it belongs to.
30+
Pick snippets from document, return a list of snippet and the related node.
31+
32+
As :class:`Snippet` can not hold any refs to doctree, we additionly returns
33+
the related nodes here. To ensure the caller can back reference to original
34+
document node and do more things (e.g. generate title path).
3235
"""
3336
# FIXME: Why doctree.source is always None?
3437
if not doctree.attributes.get('source'):
35-
logger.debug('Skipped document without source')
38+
logger.debug('Skip document without source')
3639
return []
3740

3841
metadata = app.env.metadata.get(docname, {})
3942
if 'no-search' in metadata or 'nosearch' in metadata:
40-
logger.debug('Skipped document with nosearch metadata')
43+
logger.debug('Skip document with nosearch metadata')
4144
return []
4245

43-
snippets: list[tuple[Snippet, nodes.section]] = []
44-
45-
# Pick document
46-
toplevel_section = doctree.next_node(nodes.section)
47-
if toplevel_section:
48-
snippets.append((Document(doctree), toplevel_section))
49-
else:
50-
logger.warning('can not pick document without child section: %s', doctree)
51-
52-
# Pick sections
53-
section_picker = SectionPicker(doctree)
54-
doctree.walkabout(section_picker)
55-
snippets.extend(section_picker.sections)
46+
# Walk doctree and pick snippets.
47+
picker = SnippetPicker(doctree)
48+
doctree.walkabout(picker)
5649

57-
return snippets
50+
return picker.snippets
5851

5952

60-
class SectionPicker(nodes.SparseNodeVisitor):
53+
class SnippetPicker(nodes.SparseNodeVisitor):
6154
"""Node visitor for picking snippets from document."""
6255

63-
#: Constant list of unsupported languages (:class:`pygments.lexers.Lexer`)
64-
UNSUPPORTED_LANGUAGES: list[str] = ['default']
56+
#: List of picked snippets and the section it belongs to
57+
snippets: list[tuple[Snippet, nodes.Element]]
6558

66-
#: List of picked section snippets and the section it belongs to
67-
sections: list[tuple[Section, nodes.section]]
59+
#: Stack of nested sections.
60+
_sections: list[nodes.section]
6861

69-
_section_has_code_block: bool
70-
_section_level: int
71-
72-
def __init__(self, document: nodes.document) -> None:
73-
super().__init__(document)
74-
self.sections = []
75-
self._section_has_code_block = False
76-
self._section_level = 0
62+
def __init__(self, doctree: nodes.document) -> None:
63+
super().__init__(doctree)
64+
self.snippets = []
65+
self._sections = []
7766

7867
###################
7968
# Visitor methods #
8069
###################
8170

8271
def visit_literal_block(self, node: nodes.literal_block) -> None:
83-
if node['language'] in self.UNSUPPORTED_LANGUAGES:
72+
try:
73+
code = Code(node)
74+
except ValueError as e:
75+
logger.debug(f'skip {node}: {e}')
8476
raise nodes.SkipNode
85-
self._has_code_block = True
77+
self.snippets.append((code, node))
8678

8779
def visit_section(self, node: nodes.section) -> None:
88-
self._section_level += 1
80+
self._sections.append(node)
8981

9082
def depart_section(self, node: nodes.section) -> None:
91-
self._section_level -= 1
92-
self._has_code_block = False
83+
section = self._sections.pop()
84+
assert section == node
9385

9486
# Skip non-leaf section without content
9587
if self._is_empty_non_leaf_section(node):
9688
return
97-
# Skip toplevel section, we generate :class:`Document` for it
98-
if self._section_level == 0:
99-
return
100-
101-
# TODO: code block
102-
self.sections.append((Section(node), node))
89+
if len(self._sections) == 0:
90+
self.snippets.append((Document(self.document), node))
91+
else:
92+
self.snippets.append((Section(node), node))
10393

10494
def unknown_visit(self, node: nodes.Node) -> None:
10595
pass # Ignore any unknown node

0 commit comments

Comments
 (0)