Skip to content

Commit 6a3401e

Browse files
authored
feat: Support custom index generation by schema.Classifier (#37)
Date Index is supported too now :D
1 parent 9f048e1 commit 6a3401e

File tree

11 files changed

+421
-166
lines changed

11 files changed

+421
-166
lines changed

docs/_schemas/cat.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
cat = Schema(
55
'cat',
6-
name=Field(referenceable=True, form=Field.Form.LINES),
6+
name=Field(ref=True, form=Field.Forms.LINES),
77
attrs={
8-
'id': Field(unique=True, referenceable=True, required=True),
9-
'color': Field(referenceable=True),
8+
'id': Field(uniq=True, ref=True, required=True),
9+
'color': Field(ref=True),
1010
'picture': Field(),
1111
},
1212
description_template=dedent("""

docs/_schemas/dog1.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
dog = Schema(
55
'dog',
66
attrs={
7-
'breed': Field(referenceable=True),
8-
'color': Field(referenceable=True, form=Field.Form.WORDS),
7+
'breed': Field(ref=True),
8+
'color': Field(ref=True, form=Field.Forms.WORDS),
99
},
1010
description_template=dedent("""
1111
:Breed: {{ breed }}

docs/_schemas/dog2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
dog = Schema(
55
'dog',
66
attrs={
7-
'breed': Field(referenceable=True),
8-
'color': Field(referenceable=True, form=Field.Form.WORDS),
7+
'breed': Field(ref=True),
8+
'color': Field(ref=True, form=Field.Forms.WORDS),
99
},
1010
description_template=dedent("""
1111
:Breed: :any:dog.breed:`{{ breed }}`

docs/_schemas/tmplvar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
tmplvar = Schema(
55
'tmplvar',
6-
name=Field(unique=True, referenceable=True),
6+
name=Field(uniq=True, ref=True),
77
attrs={
88
'type': Field(),
99
'conf': Field(),

docs/conf.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,35 +114,37 @@
114114

115115
#
116116
# DOG FOOD CONFIGURATION START
117-
from any import Schema, Field as F
117+
from any import Schema, Field as F, DateClassifier
118118
sys.path.insert(0, os.path.abspath('.'))
119119

120120
version_schema = Schema('version',
121-
name=F(unique=True, referenceable=True, required=True, form=F.Form.LINES),
122-
attrs={'date': F(referenceable=True)},
123-
content=F(form=F.Form.LINES),
121+
name=F(uniq=True, ref=True, required=True, form=F.Forms.LINES),
122+
attrs={
123+
'date': F(ref=True, classifiers=[DateClassifier(['%Y-%m-%d'])]),
124+
},
125+
content=F(form=F.Forms.LINES),
124126
description_template=open('_templates/version.rst', 'r').read(),
125127
reference_template='🏷️{{ title }}',
126128
missing_reference_template='🏷️{{ title }}',
127129
ambiguous_reference_template='🏷️{{ title }}')
128130
confval_schema = Schema('confval',
129-
name=F(unique=True, referenceable=True, required=True, form=F.Form.LINES),
131+
name=F(uniq=True, ref=True, required=True, form=F.Forms.LINES),
130132
attrs={
131133
'type': F(),
132134
'default': F(),
133-
'choice': F(form=F.Form.WORDS),
135+
'choice': F(form=F.Forms.WORDS),
134136
'versionadded': F(),
135-
'versionchanged': F(form=F.Form.LINES),
137+
'versionchanged': F(form=F.Forms.LINES),
136138
},
137139
content=F(),
138140
description_template=open('_templates/confval.rst', 'r').read(),
139141
reference_template='⚙️{{ title }}',
140142
missing_reference_template='⚙️{{ title }}',
141143
ambiguous_reference_template='⚙️{{ title }}')
142144
example_schema = Schema('example',
143-
name=F(referenceable=True),
145+
name=F(ref=True),
144146
attrs={'style': F()},
145-
content=F(form=F.Form.LINES),
147+
content=F(form=F.Forms.LINES),
146148
description_template=open('_templates/example.rst', 'r').read(),
147149
reference_template='📝{{ title }}',
148150
missing_reference_template='📝{{ title }}',

docs/usage.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ The necessary python classes for writing schema are listed here:
3030

3131
|
3232
33-
.. autoclass:: any.Field.Form
33+
.. autoclass:: any.Field.Forms
3434

35-
.. autoattribute:: any.Field.Form.PLAIN
36-
.. autoattribute:: any.Field.Form.WORDS
37-
.. autoattribute:: any.Field.Form.LINES
35+
.. autoattribute:: any.Field.Forms.PLAIN
36+
.. autoattribute:: any.Field.Forms.WORDS
37+
.. autoattribute:: any.Field.Forms.LINES
3838

3939
Documenting Object
4040
==================

src/sphinxnotes/any/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from .template import Environment as TemplateEnvironment
1616
from .domain import AnyDomain, warn_missing_reference
17-
from .schema import Schema, Field
17+
from .schema import Schema, Field, DateClassifier
1818

1919
if TYPE_CHECKING:
2020
from sphinx.application import Sphinx
@@ -24,9 +24,10 @@
2424

2525
logger = logging.getLogger(__name__)
2626

27-
# Export
27+
# Re-Export
2828
Field = Field
2929
Schema = Schema
30+
DateClassifier = DateClassifier
3031

3132

3233
def _config_inited(app: Sphinx, config: Config) -> None:

src/sphinxnotes/any/domain.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from sphinx.util import logging
1919
from sphinx.util.nodes import make_refnode
2020

21-
from .schema import Schema, Object
21+
from .schema import Schema, Object, PlainClassifier
2222
from .directives import AnyDirective
2323
from .roles import AnyRole
2424
from .indices import AnyIndex
@@ -176,29 +176,44 @@ def add_schema(cls, schema: Schema) -> None:
176176
# Add to schemas dict
177177
cls._schemas[schema.objtype] = schema
178178

179-
# Generates reftypes(role names) for all referenceable fields
180179
reftypes = [schema.objtype]
181-
for name, field, _ in schema.fields_of(None):
182-
if field.referenceable:
183-
reftypes.append(objtype_and_objfield_to_reftype(schema.objtype, name))
184-
185-
# Roles is used for converting role name to corrsponding objtype
186-
cls.object_types[schema.objtype] = ObjType(schema.objtype, *reftypes)
187-
cls.directives[schema.objtype] = AnyDirective.derive(schema)
188-
for r in reftypes:
189-
# Create role for referencing object (by various fields)
190-
_, field = reftype_to_objtype_and_objfield(r)
191-
cls.roles[r] = AnyRole.derive(schema, field)(
180+
for name, field in schema.fields(all=False):
181+
if not field.ref:
182+
continue
183+
184+
# Generates reftypes for all referenceable fields
185+
# For later use when generating roles and indices.
186+
reftype = objtype_and_objfield_to_reftype(schema.objtype, name)
187+
reftypes.append(reftype)
188+
189+
for reftype in reftypes:
190+
_, field = reftype_to_objtype_and_objfield(reftype)
191+
# Create role for referencing object by field
192+
cls.roles[reftype] = AnyRole.derive(schema, field)(
192193
# Emit warning when missing reference (node['refwarn'] = True)
193194
warn_dangling=True,
194195
# Inner node (contnode) would be replaced in resolve_xref method,
195196
# so fix its class.
196197
innernodeclass=literal,
197198
)
198199

199-
index = AnyIndex.derive(schema, field)
200-
cls.indices.append(index)
201-
cls._indices_for_reftype[r] = index
200+
# FIXME: name and content can not be index now
201+
if field is not None:
202+
classifiers = schema.attrs[field].classifiers
203+
elif schema.name is not None:
204+
classifiers = schema.name.classifiers
205+
else:
206+
classifiers = [PlainClassifier()]
207+
# Generates index for indexing object by fields
208+
for indexer in classifiers:
209+
index = AnyIndex.derive(schema, field, indexer)
210+
cls.indices.append(index)
211+
cls._indices_for_reftype[reftype] = index # TODO: mulitple catelogers.
212+
213+
# TODO: document
214+
cls.object_types[schema.objtype] = ObjType(schema.objtype, *reftypes)
215+
# Generates directive for creating object.
216+
cls.directives[schema.objtype] = AnyDirective.derive(schema)
202217

203218
def _get_index_anchor(self, reftype: str, refval: str) -> tuple[str, str]:
204219
"""

src/sphinxnotes/any/indices.py

Lines changed: 99 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88
:license: BSD, see LICENSE for details.
99
"""
1010

11-
from typing import Iterable
11+
from typing import Iterable, TypeVar
1212
import re
1313

14-
from sphinx.domains import Index, IndexEntry
14+
from sphinx.domains import Domain, Index, IndexEntry
1515
from sphinx.util import logging
1616
from docutils import core, nodes
1717
from docutils.parsers.rst import roles
1818

19-
from .schema import Schema
19+
from .schema import Schema, Value, Classifier, Classif
2020

2121
logger = logging.getLogger(__name__)
2222

@@ -26,20 +26,20 @@ class AnyIndex(Index):
2626
Index subclass to provide the object reference index.
2727
"""
2828

29+
domain: Domain # for type hint
2930
schema: Schema
30-
# TODO: document
3131
field: str | None = None
32-
33-
name: str
34-
localname: str
35-
shortname: str
32+
classifier: Classifier
3633

3734
@classmethod
38-
def derive(cls, schema: Schema, field: str | None = None) -> type['AnyIndex']:
35+
def derive(
36+
cls, schema: Schema, field: str | None, classifier: Classifier
37+
) -> type['AnyIndex']:
3938
"""Generate an AnyIndex child class for indexing object."""
39+
# TODO: add Indexer.name
4040
if field:
4141
typ = f'Any{schema.objtype.title()}{field.title()}Index'
42-
name = schema.objtype + '.' + field
42+
name = schema.objtype + '.' + field # TOOD: objtype_and_objfield_to_reftype
4343
localname = f'{schema.objtype.title()} {field.title()} Reference Index'
4444
else:
4545
typ = f'Any{schema.objtype.title()}Index'
@@ -49,65 +49,116 @@ def derive(cls, schema: Schema, field: str | None = None) -> type['AnyIndex']:
4949
typ,
5050
(cls,),
5151
{
52-
'schema': schema,
53-
'field': field,
5452
'name': name,
5553
'localname': localname,
5654
'shortname': 'references',
55+
'schema': schema,
56+
'field': field,
57+
'classifier': classifier,
5758
},
5859
)
5960

6061
def generate(
6162
self, docnames: Iterable[str] | None = None
6263
) -> tuple[list[tuple[str, list[IndexEntry]]], bool]:
6364
"""Override parent method."""
64-
content = {} # type: dict[str, list[IndexEntry]]
65-
# list of all references
66-
objrefs = sorted(self.domain.data['references'].items())
6765

68-
# Reference value -> object IDs
69-
objs_with_same_ref: dict[str, set[str]] = {}
66+
# Single index for generating normal entries (subtype=0).
67+
# Category (lv1) → Category (for ordering objids) → objids
68+
singleidx: dict[Classif, dict[Classif, set[str]]] = {}
69+
# Dual index for generating entrie (subtype=1) and its sub-entries (subtype=2).
70+
# Category (lv1) → Category (lv2) → Category (for ordering objids) → objids
71+
dualidx: dict[Classif, dict[Classif, dict[Classif, set[str]]]] = {}
7072

73+
objrefs = sorted(self.domain.data['references'].items())
7174
for (objtype, objfield, objref), objids in objrefs:
7275
if objtype != self.schema.objtype:
7376
continue
7477
if self.field and objfield != self.field:
7578
continue
76-
objs = objs_with_same_ref.setdefault(objref, set())
77-
objs.update(objids)
78-
79-
for objref, objids in sorted(objs_with_same_ref.items()):
80-
# Add a entry for objref
81-
# 1: Entry with sub-entries.
82-
entries = content.setdefault(objref, [])
83-
for objid in sorted(objids):
84-
docname, anchor, obj = self.domain.data['objects'][
85-
self.schema.objtype, objid
86-
]
87-
if docnames and docname not in docnames:
88-
continue
89-
name = self.schema.title_of(obj) or objid
90-
extra = '' if name == objid else objid
91-
objcont = self.schema.content_of(obj)
92-
if isinstance(objcont, str):
93-
desc = objcont
94-
elif isinstance(objcont, list):
95-
desc = '\n'.join(objcont)
79+
80+
# TODO: pass a real value
81+
for catelog in self.classifier.classify(Value(objref)):
82+
category = catelog.as_category()
83+
entry = catelog.as_entry()
84+
if entry is None:
85+
singleidx.setdefault(category, {}).setdefault(
86+
catelog, set()
87+
).update(objids)
9688
else:
97-
desc = ''
98-
desc = strip_rst_markups(desc) # strip rst markups
99-
desc = ''.join(
100-
[ln for ln in desc.split('\n') if ln.strip()]
101-
) # strip NEWLINE
102-
desc = desc[:50] + '…' if len(desc) > 50 else desc # shorten
103-
# 0: Normal entry
104-
entries.append(IndexEntry(name, 0, docname, anchor, extra, '', desc))
105-
106-
# sort by first letter
107-
sorted_content = sorted(content.items())
89+
dualidx.setdefault(category, {}).setdefault(entry, {}).setdefault(
90+
catelog, set()
91+
).update(objids)
92+
93+
content: dict[Classif, list[IndexEntry]] = {} # category → entries
94+
for category, entries in self._sort_by_catelog(singleidx):
95+
index_entries = content.setdefault(category, [])
96+
for category, objids in self._sort_by_catelog(entries):
97+
for objid in objids:
98+
entry = self._generate_index_entry(objid, docnames, category)
99+
if entry is None:
100+
continue
101+
index_entries.append(entry)
102+
103+
for category, entries in self._sort_by_catelog(dualidx):
104+
index_entries = content.setdefault(category, [])
105+
for entry, subentries in self._sort_by_catelog(entries):
106+
index_entries.append(self._generate_empty_index_entry(entry))
107+
for subentry, objids in self._sort_by_catelog(subentries):
108+
for objid in objids:
109+
entry = self._generate_index_entry(objid, docnames, subentry)
110+
if entry is None:
111+
continue
112+
index_entries.append(entry)
113+
114+
# sort by category, and map classif -> str
115+
sorted_content = [
116+
(classif.leaf, entries)
117+
for classif, entries in self._sort_by_catelog(content)
118+
]
108119

109120
return sorted_content, False
110121

122+
def _generate_index_entry(
123+
self, objid: str, ignore_docnames: Iterable[str] | None, category: Classif
124+
) -> IndexEntry | None:
125+
docname, anchor, obj = self.domain.data['objects'][self.schema.objtype, objid]
126+
if ignore_docnames and docname not in ignore_docnames:
127+
return None
128+
name = self.schema.title_of(obj) or objid
129+
subtype = category.index_entry_subtype
130+
extra = category.leaf
131+
objcont = self.schema.content_of(obj)
132+
if isinstance(objcont, str):
133+
desc = objcont
134+
elif isinstance(objcont, list):
135+
desc = '\n'.join(objcont) # FIXME: use schema.Form
136+
else:
137+
desc = ''
138+
desc = strip_rst_markups(desc) # strip rst markups
139+
desc = ''.join([ln for ln in desc.split('\n') if ln.strip()]) # strip NEWLINE
140+
desc = desc[:50] + '…' if len(desc) > 50 else desc # shorten
141+
return IndexEntry(
142+
name, # the name of the index entry to be displayed
143+
subtype, # the sub-entry related type
144+
docname, # docname where the entry is located
145+
anchor, # anchor for the entry within docname
146+
extra, # extra info for the entry
147+
'', # qualifier for the description
148+
desc, # description for the entry
149+
)
150+
151+
def _generate_empty_index_entry(self, category: Classif) -> IndexEntry:
152+
return IndexEntry(
153+
category.leaf, category.index_entry_subtype, '', '', '', '', ''
154+
)
155+
156+
_T = TypeVar('_T')
157+
158+
def _sort_by_catelog(self, d: dict[Classif, _T]) -> list[tuple[Classif, _T]]:
159+
"""Helper for sorting dict items by Category."""
160+
return self.classifier.sort(d.items(), lambda x: x[0])
161+
111162

112163
def strip_rst_markups(rst: str) -> str:
113164
"""Strip markups and newlines in rST.

0 commit comments

Comments
 (0)