julee 0.1.5__py3-none-any.whl → 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- julee/docs/sphinx_hcd/__init__.py +146 -13
- julee/docs/sphinx_hcd/domain/__init__.py +5 -0
- julee/docs/sphinx_hcd/domain/models/__init__.py +32 -0
- julee/docs/sphinx_hcd/domain/models/accelerator.py +152 -0
- julee/docs/sphinx_hcd/domain/models/app.py +151 -0
- julee/docs/sphinx_hcd/domain/models/code_info.py +121 -0
- julee/docs/sphinx_hcd/domain/models/epic.py +79 -0
- julee/docs/sphinx_hcd/domain/models/integration.py +230 -0
- julee/docs/sphinx_hcd/domain/models/journey.py +222 -0
- julee/docs/sphinx_hcd/domain/models/persona.py +106 -0
- julee/docs/sphinx_hcd/domain/models/story.py +128 -0
- julee/docs/sphinx_hcd/domain/repositories/__init__.py +25 -0
- julee/docs/sphinx_hcd/domain/repositories/accelerator.py +98 -0
- julee/docs/sphinx_hcd/domain/repositories/app.py +57 -0
- julee/docs/sphinx_hcd/domain/repositories/base.py +89 -0
- julee/docs/sphinx_hcd/domain/repositories/code_info.py +69 -0
- julee/docs/sphinx_hcd/domain/repositories/epic.py +62 -0
- julee/docs/sphinx_hcd/domain/repositories/integration.py +79 -0
- julee/docs/sphinx_hcd/domain/repositories/journey.py +106 -0
- julee/docs/sphinx_hcd/domain/repositories/story.py +68 -0
- julee/docs/sphinx_hcd/domain/use_cases/__init__.py +64 -0
- julee/docs/sphinx_hcd/domain/use_cases/derive_personas.py +166 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_accelerator_references.py +236 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_app_references.py +144 -0
- julee/docs/sphinx_hcd/domain/use_cases/resolve_story_references.py +121 -0
- julee/docs/sphinx_hcd/parsers/__init__.py +48 -0
- julee/docs/sphinx_hcd/parsers/ast.py +150 -0
- julee/docs/sphinx_hcd/parsers/gherkin.py +155 -0
- julee/docs/sphinx_hcd/parsers/yaml.py +184 -0
- julee/docs/sphinx_hcd/repositories/__init__.py +4 -0
- julee/docs/sphinx_hcd/repositories/memory/__init__.py +25 -0
- julee/docs/sphinx_hcd/repositories/memory/accelerator.py +86 -0
- julee/docs/sphinx_hcd/repositories/memory/app.py +45 -0
- julee/docs/sphinx_hcd/repositories/memory/base.py +106 -0
- julee/docs/sphinx_hcd/repositories/memory/code_info.py +59 -0
- julee/docs/sphinx_hcd/repositories/memory/epic.py +54 -0
- julee/docs/sphinx_hcd/repositories/memory/integration.py +70 -0
- julee/docs/sphinx_hcd/repositories/memory/journey.py +96 -0
- julee/docs/sphinx_hcd/repositories/memory/story.py +63 -0
- julee/docs/sphinx_hcd/sphinx/__init__.py +28 -0
- julee/docs/sphinx_hcd/sphinx/adapters.py +116 -0
- julee/docs/sphinx_hcd/sphinx/context.py +163 -0
- julee/docs/sphinx_hcd/sphinx/directives/__init__.py +160 -0
- julee/docs/sphinx_hcd/sphinx/directives/accelerator.py +576 -0
- julee/docs/sphinx_hcd/sphinx/directives/app.py +349 -0
- julee/docs/sphinx_hcd/sphinx/directives/base.py +211 -0
- julee/docs/sphinx_hcd/sphinx/directives/epic.py +434 -0
- julee/docs/sphinx_hcd/sphinx/directives/integration.py +220 -0
- julee/docs/sphinx_hcd/sphinx/directives/journey.py +642 -0
- julee/docs/sphinx_hcd/sphinx/directives/persona.py +345 -0
- julee/docs/sphinx_hcd/sphinx/directives/story.py +575 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/__init__.py +16 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/builder_inited.py +31 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_read.py +27 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_resolved.py +43 -0
- julee/docs/sphinx_hcd/sphinx/event_handlers/env_purge_doc.py +42 -0
- julee/docs/sphinx_hcd/sphinx/initialization.py +139 -0
- julee/docs/sphinx_hcd/tests/__init__.py +9 -0
- julee/docs/sphinx_hcd/tests/conftest.py +6 -0
- julee/docs/sphinx_hcd/tests/domain/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/models/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_accelerator.py +266 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_app.py +258 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_code_info.py +231 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_epic.py +163 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_integration.py +327 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_journey.py +249 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_persona.py +172 -0
- julee/docs/sphinx_hcd/tests/domain/models/test_story.py +216 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_derive_personas.py +314 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_accelerator_references.py +476 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_app_references.py +265 -0
- julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_story_references.py +229 -0
- julee/docs/sphinx_hcd/tests/integration/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/parsers/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/parsers/test_ast.py +298 -0
- julee/docs/sphinx_hcd/tests/parsers/test_gherkin.py +282 -0
- julee/docs/sphinx_hcd/tests/parsers/test_yaml.py +496 -0
- julee/docs/sphinx_hcd/tests/repositories/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/repositories/test_accelerator.py +298 -0
- julee/docs/sphinx_hcd/tests/repositories/test_app.py +218 -0
- julee/docs/sphinx_hcd/tests/repositories/test_base.py +151 -0
- julee/docs/sphinx_hcd/tests/repositories/test_code_info.py +253 -0
- julee/docs/sphinx_hcd/tests/repositories/test_epic.py +237 -0
- julee/docs/sphinx_hcd/tests/repositories/test_integration.py +268 -0
- julee/docs/sphinx_hcd/tests/repositories/test_journey.py +294 -0
- julee/docs/sphinx_hcd/tests/repositories/test_story.py +236 -0
- julee/docs/sphinx_hcd/tests/sphinx/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/sphinx/directives/__init__.py +1 -0
- julee/docs/sphinx_hcd/tests/sphinx/directives/test_base.py +160 -0
- julee/docs/sphinx_hcd/tests/sphinx/test_adapters.py +176 -0
- julee/docs/sphinx_hcd/tests/sphinx/test_context.py +257 -0
- {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/METADATA +2 -1
- {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/RECORD +98 -13
- julee/docs/sphinx_hcd/accelerators.py +0 -1175
- julee/docs/sphinx_hcd/apps.py +0 -518
- julee/docs/sphinx_hcd/epics.py +0 -453
- julee/docs/sphinx_hcd/integrations.py +0 -310
- julee/docs/sphinx_hcd/journeys.py +0 -797
- julee/docs/sphinx_hcd/personas.py +0 -457
- julee/docs/sphinx_hcd/stories.py +0 -960
- {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/WHEEL +0 -0
- {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""App directives for sphinx_hcd.
|
|
2
|
+
|
|
3
|
+
Provides directives for rendering application information:
|
|
4
|
+
- define-app: Render app info from YAML manifest + derived data
|
|
5
|
+
- app-index: Generate index tables grouped by type
|
|
6
|
+
- apps-for-persona: List apps for a persona
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from docutils import nodes
|
|
10
|
+
|
|
11
|
+
from ...domain.models.app import App, AppType
|
|
12
|
+
from ...domain.use_cases import (
|
|
13
|
+
get_epics_for_app,
|
|
14
|
+
get_journeys_for_app,
|
|
15
|
+
get_personas_for_app,
|
|
16
|
+
get_stories_for_app,
|
|
17
|
+
)
|
|
18
|
+
from ...utils import normalize_name, path_to_root, slugify
|
|
19
|
+
from .base import HCDDirective
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DefineAppPlaceholder(nodes.General, nodes.Element):
|
|
23
|
+
"""Placeholder node for define-app, replaced at doctree-resolved."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AppIndexPlaceholder(nodes.General, nodes.Element):
|
|
29
|
+
"""Placeholder node for app-index, replaced at doctree-resolved."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AppsForPersonaPlaceholder(nodes.General, nodes.Element):
|
|
35
|
+
"""Placeholder node for apps-for-persona, replaced at doctree-resolved."""
|
|
36
|
+
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DefineAppDirective(HCDDirective):
|
|
41
|
+
"""Render app info from YAML manifest plus derived data.
|
|
42
|
+
|
|
43
|
+
Usage::
|
|
44
|
+
|
|
45
|
+
.. define-app:: credential-tool
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
required_arguments = 1
|
|
49
|
+
|
|
50
|
+
def run(self):
|
|
51
|
+
app_slug = self.arguments[0]
|
|
52
|
+
|
|
53
|
+
# Track documented apps in environment (for validation)
|
|
54
|
+
if not hasattr(self.env, "documented_apps"):
|
|
55
|
+
self.env.documented_apps = set()
|
|
56
|
+
self.env.documented_apps.add(app_slug)
|
|
57
|
+
|
|
58
|
+
# Return placeholder - rendering in doctree-resolved
|
|
59
|
+
node = DefineAppPlaceholder()
|
|
60
|
+
node["app_slug"] = app_slug
|
|
61
|
+
return [node]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AppIndexDirective(HCDDirective):
|
|
65
|
+
"""Generate index tables grouped by app type.
|
|
66
|
+
|
|
67
|
+
Usage::
|
|
68
|
+
|
|
69
|
+
.. app-index::
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def run(self):
|
|
73
|
+
node = AppIndexPlaceholder()
|
|
74
|
+
return [node]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AppsForPersonaDirective(HCDDirective):
|
|
78
|
+
"""List apps for a specific persona.
|
|
79
|
+
|
|
80
|
+
Usage::
|
|
81
|
+
|
|
82
|
+
.. apps-for-persona:: Member Implementer
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
required_arguments = 1
|
|
86
|
+
final_argument_whitespace = True
|
|
87
|
+
|
|
88
|
+
def run(self):
|
|
89
|
+
node = AppsForPersonaPlaceholder()
|
|
90
|
+
node["persona"] = self.arguments[0]
|
|
91
|
+
return [node]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_app_content(app_slug: str, docname: str, hcd_context):
|
|
95
|
+
"""Build the content nodes for an app."""
|
|
96
|
+
from sphinx.addnodes import seealso
|
|
97
|
+
|
|
98
|
+
from ...config import get_config
|
|
99
|
+
|
|
100
|
+
config = get_config()
|
|
101
|
+
prefix = path_to_root(docname)
|
|
102
|
+
|
|
103
|
+
# Get app from repository
|
|
104
|
+
app = hcd_context.app_repo.get(app_slug)
|
|
105
|
+
if not app:
|
|
106
|
+
para = nodes.paragraph()
|
|
107
|
+
para += nodes.problematic(text=f"App '{app_slug}' not found in apps/")
|
|
108
|
+
return [para]
|
|
109
|
+
|
|
110
|
+
# Get all entities for cross-references
|
|
111
|
+
all_stories = hcd_context.story_repo.list_all()
|
|
112
|
+
all_epics = hcd_context.epic_repo.list_all()
|
|
113
|
+
all_journeys = hcd_context.journey_repo.list_all()
|
|
114
|
+
|
|
115
|
+
result_nodes = []
|
|
116
|
+
|
|
117
|
+
# Description first
|
|
118
|
+
if app.description:
|
|
119
|
+
desc_para = nodes.paragraph()
|
|
120
|
+
desc_para += nodes.Text(app.description)
|
|
121
|
+
result_nodes.append(desc_para)
|
|
122
|
+
|
|
123
|
+
# Stories count and link
|
|
124
|
+
app_stories = get_stories_for_app(app, all_stories)
|
|
125
|
+
|
|
126
|
+
if app_stories:
|
|
127
|
+
story_count = len(app_stories)
|
|
128
|
+
stories_para = nodes.paragraph()
|
|
129
|
+
stories_para += nodes.Text(f"The {app.name} has ")
|
|
130
|
+
story_path = f"{prefix}{config.get_doc_path('stories')}/{app_slug}.html"
|
|
131
|
+
ref = nodes.reference("", "", refuri=story_path)
|
|
132
|
+
ref += nodes.Text(f"{story_count} stories")
|
|
133
|
+
stories_para += ref
|
|
134
|
+
stories_para += nodes.Text(".")
|
|
135
|
+
result_nodes.append(stories_para)
|
|
136
|
+
|
|
137
|
+
# Build seealso box with metadata
|
|
138
|
+
seealso_node = seealso()
|
|
139
|
+
|
|
140
|
+
# Type
|
|
141
|
+
type_para = nodes.paragraph()
|
|
142
|
+
type_para += nodes.strong(text="Type: ")
|
|
143
|
+
type_para += nodes.Text(app.type_label)
|
|
144
|
+
seealso_node += type_para
|
|
145
|
+
|
|
146
|
+
# Status (if present)
|
|
147
|
+
if app.status:
|
|
148
|
+
status_para = nodes.paragraph()
|
|
149
|
+
status_para += nodes.strong(text="Status: ")
|
|
150
|
+
status_para += nodes.Text(app.status)
|
|
151
|
+
seealso_node += status_para
|
|
152
|
+
|
|
153
|
+
# Personas (derived from stories)
|
|
154
|
+
personas = get_personas_for_app(app, all_stories, all_epics)
|
|
155
|
+
if personas:
|
|
156
|
+
persona_para = nodes.paragraph()
|
|
157
|
+
persona_para += nodes.strong(text="Personas: ")
|
|
158
|
+
for i, persona in enumerate(personas):
|
|
159
|
+
persona_slug = slugify(persona.name)
|
|
160
|
+
persona_path = (
|
|
161
|
+
f"{prefix}{config.get_doc_path('personas')}/{persona_slug}.html"
|
|
162
|
+
)
|
|
163
|
+
ref = nodes.reference("", "", refuri=persona_path)
|
|
164
|
+
ref += nodes.Text(persona.name)
|
|
165
|
+
persona_para += ref
|
|
166
|
+
if i < len(personas) - 1:
|
|
167
|
+
persona_para += nodes.Text(", ")
|
|
168
|
+
seealso_node += persona_para
|
|
169
|
+
|
|
170
|
+
# Related Journeys
|
|
171
|
+
journeys = get_journeys_for_app(app, all_stories, all_journeys)
|
|
172
|
+
if journeys:
|
|
173
|
+
journey_para = nodes.paragraph()
|
|
174
|
+
journey_para += nodes.strong(text="Journeys: ")
|
|
175
|
+
for i, journey in enumerate(journeys):
|
|
176
|
+
journey_path = (
|
|
177
|
+
f"{prefix}{config.get_doc_path('journeys')}/{journey.slug}.html"
|
|
178
|
+
)
|
|
179
|
+
ref = nodes.reference("", "", refuri=journey_path)
|
|
180
|
+
ref += nodes.Text(journey.slug.replace("-", " ").title())
|
|
181
|
+
journey_para += ref
|
|
182
|
+
if i < len(journeys) - 1:
|
|
183
|
+
journey_para += nodes.Text(", ")
|
|
184
|
+
seealso_node += journey_para
|
|
185
|
+
|
|
186
|
+
# Related Epics
|
|
187
|
+
epics = get_epics_for_app(app, all_stories, all_epics)
|
|
188
|
+
if epics:
|
|
189
|
+
epic_para = nodes.paragraph()
|
|
190
|
+
epic_para += nodes.strong(text="Epics: ")
|
|
191
|
+
for i, epic in enumerate(epics):
|
|
192
|
+
epic_path = f"{prefix}{config.get_doc_path('epics')}/{epic.slug}.html"
|
|
193
|
+
ref = nodes.reference("", "", refuri=epic_path)
|
|
194
|
+
ref += nodes.Text(epic.slug.replace("-", " ").title())
|
|
195
|
+
epic_para += ref
|
|
196
|
+
if i < len(epics) - 1:
|
|
197
|
+
epic_para += nodes.Text(", ")
|
|
198
|
+
seealso_node += epic_para
|
|
199
|
+
|
|
200
|
+
result_nodes.append(seealso_node)
|
|
201
|
+
|
|
202
|
+
return result_nodes
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def build_app_index(docname: str, hcd_context):
|
|
206
|
+
"""Build the app index grouped by type."""
|
|
207
|
+
all_apps = hcd_context.app_repo.list_all()
|
|
208
|
+
all_stories = hcd_context.story_repo.list_all()
|
|
209
|
+
|
|
210
|
+
if not all_apps:
|
|
211
|
+
para = nodes.paragraph()
|
|
212
|
+
para += nodes.emphasis(text="No apps defined")
|
|
213
|
+
return [para]
|
|
214
|
+
|
|
215
|
+
# Group apps by type
|
|
216
|
+
by_type: dict[AppType, list[App]] = {
|
|
217
|
+
AppType.STAFF: [],
|
|
218
|
+
AppType.EXTERNAL: [],
|
|
219
|
+
AppType.MEMBER_TOOL: [],
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for app in all_apps:
|
|
223
|
+
if app.app_type in by_type:
|
|
224
|
+
by_type[app.app_type].append(app)
|
|
225
|
+
else:
|
|
226
|
+
by_type.setdefault(app.app_type, []).append(app)
|
|
227
|
+
|
|
228
|
+
result_nodes = []
|
|
229
|
+
|
|
230
|
+
type_sections = [
|
|
231
|
+
(AppType.STAFF, "Staff Applications"),
|
|
232
|
+
(AppType.EXTERNAL, "External Applications"),
|
|
233
|
+
(AppType.MEMBER_TOOL, "Member Tools"),
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
for type_key, type_label in type_sections:
|
|
237
|
+
apps = by_type.get(type_key, [])
|
|
238
|
+
if not apps:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
# Section heading
|
|
242
|
+
heading = nodes.paragraph()
|
|
243
|
+
heading += nodes.strong(text=type_label)
|
|
244
|
+
result_nodes.append(heading)
|
|
245
|
+
|
|
246
|
+
# App list
|
|
247
|
+
app_list = nodes.bullet_list()
|
|
248
|
+
|
|
249
|
+
for app in sorted(apps, key=lambda a: a.name):
|
|
250
|
+
# Get personas for this app
|
|
251
|
+
app_stories = get_stories_for_app(app, all_stories)
|
|
252
|
+
personas = {s.persona for s in app_stories}
|
|
253
|
+
|
|
254
|
+
item = nodes.list_item()
|
|
255
|
+
para = nodes.paragraph()
|
|
256
|
+
|
|
257
|
+
# Link to app
|
|
258
|
+
app_path = f"{app.slug}.html"
|
|
259
|
+
ref = nodes.reference("", "", refuri=app_path)
|
|
260
|
+
ref += nodes.Text(app.name)
|
|
261
|
+
para += ref
|
|
262
|
+
|
|
263
|
+
# Personas
|
|
264
|
+
if personas:
|
|
265
|
+
para += nodes.Text(f" ({', '.join(sorted(personas))})")
|
|
266
|
+
|
|
267
|
+
item += para
|
|
268
|
+
app_list += item
|
|
269
|
+
|
|
270
|
+
result_nodes.append(app_list)
|
|
271
|
+
|
|
272
|
+
return result_nodes
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def build_apps_for_persona(docname: str, persona_arg: str, hcd_context):
|
|
276
|
+
"""Build list of apps for a persona."""
|
|
277
|
+
from ...config import get_config
|
|
278
|
+
from ...domain.use_cases import derive_personas, get_apps_for_persona
|
|
279
|
+
|
|
280
|
+
config = get_config()
|
|
281
|
+
prefix = path_to_root(docname)
|
|
282
|
+
persona_normalized = normalize_name(persona_arg)
|
|
283
|
+
|
|
284
|
+
all_apps = hcd_context.app_repo.list_all()
|
|
285
|
+
all_stories = hcd_context.story_repo.list_all()
|
|
286
|
+
all_epics = hcd_context.epic_repo.list_all()
|
|
287
|
+
|
|
288
|
+
# Derive personas
|
|
289
|
+
personas = derive_personas(all_stories, all_epics)
|
|
290
|
+
|
|
291
|
+
# Find the persona
|
|
292
|
+
persona = None
|
|
293
|
+
for p in personas:
|
|
294
|
+
if p.normalized_name == persona_normalized:
|
|
295
|
+
persona = p
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
if not persona:
|
|
299
|
+
para = nodes.paragraph()
|
|
300
|
+
para += nodes.emphasis(text=f"No apps found for persona '{persona_arg}'")
|
|
301
|
+
return [para]
|
|
302
|
+
|
|
303
|
+
# Get apps for this persona
|
|
304
|
+
matching_apps = get_apps_for_persona(persona, all_apps)
|
|
305
|
+
|
|
306
|
+
if not matching_apps:
|
|
307
|
+
para = nodes.paragraph()
|
|
308
|
+
para += nodes.emphasis(text=f"No apps found for persona '{persona_arg}'")
|
|
309
|
+
return [para]
|
|
310
|
+
|
|
311
|
+
bullet_list = nodes.bullet_list()
|
|
312
|
+
|
|
313
|
+
for app in sorted(matching_apps, key=lambda a: a.name):
|
|
314
|
+
item = nodes.list_item()
|
|
315
|
+
para = nodes.paragraph()
|
|
316
|
+
|
|
317
|
+
app_path = f"{prefix}{config.get_doc_path('applications')}/{app.slug}.html"
|
|
318
|
+
ref = nodes.reference("", "", refuri=app_path)
|
|
319
|
+
ref += nodes.Text(app.name)
|
|
320
|
+
para += ref
|
|
321
|
+
|
|
322
|
+
item += para
|
|
323
|
+
bullet_list += item
|
|
324
|
+
|
|
325
|
+
return [bullet_list]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def process_app_placeholders(app, doctree, docname):
|
|
329
|
+
"""Replace app placeholders with rendered content."""
|
|
330
|
+
from ..context import get_hcd_context
|
|
331
|
+
|
|
332
|
+
hcd_context = get_hcd_context(app)
|
|
333
|
+
|
|
334
|
+
# Process define-app placeholders
|
|
335
|
+
for node in doctree.traverse(DefineAppPlaceholder):
|
|
336
|
+
app_slug = node["app_slug"]
|
|
337
|
+
content = build_app_content(app_slug, docname, hcd_context)
|
|
338
|
+
node.replace_self(content)
|
|
339
|
+
|
|
340
|
+
# Process app-index placeholders
|
|
341
|
+
for node in doctree.traverse(AppIndexPlaceholder):
|
|
342
|
+
content = build_app_index(docname, hcd_context)
|
|
343
|
+
node.replace_self(content)
|
|
344
|
+
|
|
345
|
+
# Process apps-for-persona placeholders
|
|
346
|
+
for node in doctree.traverse(AppsForPersonaPlaceholder):
|
|
347
|
+
persona = node["persona"]
|
|
348
|
+
content = build_apps_for_persona(docname, persona, hcd_context)
|
|
349
|
+
node.replace_self(content)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Base directive and utilities for sphinx_hcd directives.
|
|
2
|
+
|
|
3
|
+
Provides common functionality for building docutils nodes and accessing
|
|
4
|
+
the HCDContext repositories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from docutils import nodes
|
|
10
|
+
from sphinx.util.docutils import SphinxDirective
|
|
11
|
+
|
|
12
|
+
from ...config import get_config
|
|
13
|
+
from ...utils import path_to_root, slugify
|
|
14
|
+
from ..context import HCDContext, get_hcd_context
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HCDDirective(SphinxDirective):
|
|
21
|
+
"""Base directive with HCD context access.
|
|
22
|
+
|
|
23
|
+
All HCD directives inherit from this to get easy access to:
|
|
24
|
+
- HCDContext with all repositories
|
|
25
|
+
- Config for path resolution
|
|
26
|
+
- Common node-building utilities
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def hcd_context(self) -> HCDContext:
|
|
31
|
+
"""Get the HCD context from Sphinx app."""
|
|
32
|
+
return get_hcd_context(self.env.app)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def hcd_config(self):
|
|
36
|
+
"""Get the HCD configuration."""
|
|
37
|
+
return get_config()
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def docname(self) -> str:
|
|
41
|
+
"""Get the current document name."""
|
|
42
|
+
return self.env.docname
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def prefix(self) -> str:
|
|
46
|
+
"""Get relative path prefix to docs root."""
|
|
47
|
+
return path_to_root(self.docname)
|
|
48
|
+
|
|
49
|
+
def get_doc_path(self, doc_type: str) -> str:
|
|
50
|
+
"""Get the path for a documentation type with prefix."""
|
|
51
|
+
return f"{self.prefix}{self.hcd_config.get_doc_path(doc_type)}"
|
|
52
|
+
|
|
53
|
+
def make_link(
|
|
54
|
+
self,
|
|
55
|
+
text: str,
|
|
56
|
+
path: str,
|
|
57
|
+
strong: bool = False,
|
|
58
|
+
) -> nodes.reference:
|
|
59
|
+
"""Create a reference node with text.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
text: Link text
|
|
63
|
+
path: Target path (relative or absolute)
|
|
64
|
+
strong: Whether to make text bold
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Reference node
|
|
68
|
+
"""
|
|
69
|
+
ref = nodes.reference("", "", refuri=path)
|
|
70
|
+
if strong:
|
|
71
|
+
ref += nodes.strong(text=text)
|
|
72
|
+
else:
|
|
73
|
+
ref += nodes.Text(text)
|
|
74
|
+
return ref
|
|
75
|
+
|
|
76
|
+
def make_app_link(self, app_slug: str) -> nodes.reference:
|
|
77
|
+
"""Create a link to an app page."""
|
|
78
|
+
app_name = app_slug.replace("-", " ").title()
|
|
79
|
+
app_path = f"{self.get_doc_path('applications')}/{app_slug}.html"
|
|
80
|
+
return self.make_link(app_name, app_path)
|
|
81
|
+
|
|
82
|
+
def make_persona_link(self, persona_name: str) -> nodes.reference:
|
|
83
|
+
"""Create a link to a persona page."""
|
|
84
|
+
persona_slug = slugify(persona_name)
|
|
85
|
+
persona_path = f"{self.get_doc_path('personas')}/{persona_slug}.html"
|
|
86
|
+
return self.make_link(persona_name, persona_path)
|
|
87
|
+
|
|
88
|
+
def make_epic_link(self, epic_slug: str) -> nodes.reference:
|
|
89
|
+
"""Create a link to an epic page."""
|
|
90
|
+
epic_name = epic_slug.replace("-", " ").title()
|
|
91
|
+
epic_path = f"{self.get_doc_path('epics')}/{epic_slug}.html"
|
|
92
|
+
return self.make_link(epic_name, epic_path)
|
|
93
|
+
|
|
94
|
+
def make_journey_link(self, journey_slug: str) -> nodes.reference:
|
|
95
|
+
"""Create a link to a journey page."""
|
|
96
|
+
journey_name = journey_slug.replace("-", " ").title()
|
|
97
|
+
journey_path = f"{self.get_doc_path('journeys')}/{journey_slug}.html"
|
|
98
|
+
return self.make_link(journey_name, journey_path)
|
|
99
|
+
|
|
100
|
+
def make_story_link(
|
|
101
|
+
self,
|
|
102
|
+
story,
|
|
103
|
+
link_text: str | None = None,
|
|
104
|
+
) -> nodes.reference:
|
|
105
|
+
"""Create a link to a story on its app's story page.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
story: Story entity or dict with app, slug, i_want
|
|
109
|
+
link_text: Optional link text (defaults to i_want)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Reference node linking to story anchor
|
|
113
|
+
"""
|
|
114
|
+
# Handle both Story entities and legacy dicts
|
|
115
|
+
if hasattr(story, "app_slug"):
|
|
116
|
+
app_slug = story.app_slug
|
|
117
|
+
story_slug = story.slug
|
|
118
|
+
default_text = story.i_want
|
|
119
|
+
else:
|
|
120
|
+
app_slug = story.get("app", story.get("app_slug"))
|
|
121
|
+
story_slug = story.get("slug")
|
|
122
|
+
default_text = story.get("i_want", story.get("feature_title", ""))
|
|
123
|
+
|
|
124
|
+
config = self.hcd_config
|
|
125
|
+
target_doc = f"{config.get_doc_path('stories')}/{app_slug}"
|
|
126
|
+
ref_uri = self._build_relative_uri(target_doc, story_slug)
|
|
127
|
+
|
|
128
|
+
ref = nodes.reference("", "", refuri=ref_uri)
|
|
129
|
+
ref += nodes.Text(link_text or default_text)
|
|
130
|
+
return ref
|
|
131
|
+
|
|
132
|
+
def _build_relative_uri(
|
|
133
|
+
self,
|
|
134
|
+
target_doc: str,
|
|
135
|
+
anchor: str | None = None,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""Build a relative URI from current doc to target.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
target_doc: Target document path (without .html)
|
|
141
|
+
anchor: Optional anchor within target
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Relative URI string
|
|
145
|
+
"""
|
|
146
|
+
from_parts = self.docname.split("/")
|
|
147
|
+
target_parts = target_doc.split("/")
|
|
148
|
+
|
|
149
|
+
# Find common prefix
|
|
150
|
+
common = 0
|
|
151
|
+
for i in range(min(len(from_parts), len(target_parts))):
|
|
152
|
+
if from_parts[i] == target_parts[i]:
|
|
153
|
+
common += 1
|
|
154
|
+
else:
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
# Build relative path
|
|
158
|
+
up_levels = len(from_parts) - common - 1
|
|
159
|
+
down_path = "/".join(target_parts[common:])
|
|
160
|
+
|
|
161
|
+
if up_levels > 0:
|
|
162
|
+
rel_path = "../" * up_levels + down_path + ".html"
|
|
163
|
+
else:
|
|
164
|
+
rel_path = down_path + ".html"
|
|
165
|
+
|
|
166
|
+
if anchor:
|
|
167
|
+
return f"{rel_path}#{anchor}"
|
|
168
|
+
return rel_path
|
|
169
|
+
|
|
170
|
+
def empty_result(self, message: str) -> list[nodes.Node]:
|
|
171
|
+
"""Create an emphasized message for empty results."""
|
|
172
|
+
para = nodes.paragraph()
|
|
173
|
+
para += nodes.emphasis(text=message)
|
|
174
|
+
return [para]
|
|
175
|
+
|
|
176
|
+
def warning_node(self, message: str) -> nodes.paragraph:
|
|
177
|
+
"""Create a warning paragraph with problematic text."""
|
|
178
|
+
para = nodes.paragraph()
|
|
179
|
+
para += nodes.problematic(text=f"[{message}]")
|
|
180
|
+
return para
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def make_deprecated_directive(
|
|
184
|
+
base_class: type,
|
|
185
|
+
old_name: str,
|
|
186
|
+
new_name: str,
|
|
187
|
+
) -> type:
|
|
188
|
+
"""Create a deprecated alias directive that warns and delegates.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
base_class: The directive class to wrap
|
|
192
|
+
old_name: The deprecated directive name
|
|
193
|
+
new_name: The new directive name
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
A new directive class that warns and delegates
|
|
197
|
+
"""
|
|
198
|
+
from sphinx.util import logging
|
|
199
|
+
|
|
200
|
+
logger = logging.getLogger(__name__)
|
|
201
|
+
|
|
202
|
+
class DeprecatedDirective(base_class):
|
|
203
|
+
def run(self):
|
|
204
|
+
logger.warning(
|
|
205
|
+
f"Directive '{old_name}' is deprecated, use '{new_name}' instead. "
|
|
206
|
+
f"(in {self.env.docname})"
|
|
207
|
+
)
|
|
208
|
+
return super().run()
|
|
209
|
+
|
|
210
|
+
DeprecatedDirective.__name__ = f"Deprecated{base_class.__name__}"
|
|
211
|
+
return DeprecatedDirective
|