presentation-sanity 0.4.3__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.
Files changed (64) hide show
  1. presentation_sanity/__init__.py +3 -0
  2. presentation_sanity/__main__.py +7 -0
  3. presentation_sanity/assets/vitepress/citations.css +89 -0
  4. presentation_sanity/assets/vitepress/config.mts +277 -0
  5. presentation_sanity/assets/vitepress/theme.ts +43 -0
  6. presentation_sanity/bibliography.py +616 -0
  7. presentation_sanity/build.py +392 -0
  8. presentation_sanity/check.py +205 -0
  9. presentation_sanity/cli.py +837 -0
  10. presentation_sanity/figures_render.py +175 -0
  11. presentation_sanity/init_project.py +563 -0
  12. presentation_sanity/manifest.py +581 -0
  13. presentation_sanity/manim_render.py +250 -0
  14. presentation_sanity/markdown_meta.py +114 -0
  15. presentation_sanity/pages.py +257 -0
  16. presentation_sanity/pptx_export.py +307 -0
  17. presentation_sanity/project.py +472 -0
  18. presentation_sanity/skills/presentation-sanity/SKILL.md +100 -0
  19. presentation_sanity/skills/presentation-sanity/modes/_shared.md +86 -0
  20. presentation_sanity/skills/presentation-sanity/modes/blog.md +44 -0
  21. presentation_sanity/skills/presentation-sanity/modes/check.md +45 -0
  22. presentation_sanity/skills/presentation-sanity/modes/cite.md +41 -0
  23. presentation_sanity/skills/presentation-sanity/modes/deploy.md +54 -0
  24. presentation_sanity/skills/presentation-sanity/modes/export.md +21 -0
  25. presentation_sanity/skills/presentation-sanity/modes/figures.md +32 -0
  26. presentation_sanity/skills/presentation-sanity/modes/init.md +75 -0
  27. presentation_sanity/skills/presentation-sanity/modes/manim.md +40 -0
  28. presentation_sanity/skills/presentation-sanity/modes/new.md +45 -0
  29. presentation_sanity/skills/presentation-sanity/modes/slides.md +49 -0
  30. presentation_sanity/skills/presentation-sanity/modes/variables.md +43 -0
  31. presentation_sanity/slidev.py +153 -0
  32. presentation_sanity/template/.gitignore +21 -0
  33. presentation_sanity/template/LICENSE +21 -0
  34. presentation_sanity/template/README.md +376 -0
  35. presentation_sanity/template/manifest.yaml +126 -0
  36. presentation_sanity/template/package-lock.json +11788 -0
  37. presentation_sanity/template/package.json +30 -0
  38. presentation_sanity/template/presentations/example/blog.md +112 -0
  39. presentation_sanity/template/presentations/example/manifest.yaml +10 -0
  40. presentation_sanity/template/presentations/example/slides.md +181 -0
  41. presentation_sanity/template/presentations/example/style.css +144 -0
  42. presentation_sanity/template/public/manim/one_source.poster.png +0 -0
  43. presentation_sanity/template/public/manim/one_source.webm +0 -0
  44. presentation_sanity/template/pyproject.toml +32 -0
  45. presentation_sanity/template/refs.bib +36 -0
  46. presentation_sanity/template/scenes/one_source.py +109 -0
  47. presentation_sanity/template/shared/blog.css +19 -0
  48. presentation_sanity/template/shared/components/DataValue.vue +93 -0
  49. presentation_sanity/template/shared/components/FigureImage.vue +57 -0
  50. presentation_sanity/template/shared/components/ManimFigure.vue +124 -0
  51. presentation_sanity/template/shared/components/ProvenancePanel.vue +344 -0
  52. presentation_sanity/template/shared/composables/useProvenance.ts +19 -0
  53. presentation_sanity/template/shared/global-bottom.vue +12 -0
  54. presentation_sanity/template/shared/layouts/manim.vue +206 -0
  55. presentation_sanity/template/shared/package.json +7 -0
  56. presentation_sanity/template/shared/style.css +40 -0
  57. presentation_sanity/template/shared/vite.config.ts +42 -0
  58. presentation_sanity/template-source.json +4 -0
  59. presentation_sanity/vitepress.py +467 -0
  60. presentation_sanity-0.4.3.dist-info/METADATA +509 -0
  61. presentation_sanity-0.4.3.dist-info/RECORD +64 -0
  62. presentation_sanity-0.4.3.dist-info/WHEEL +4 -0
  63. presentation_sanity-0.4.3.dist-info/entry_points.txt +2 -0
  64. presentation_sanity-0.4.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """presentation-sanity: build slides and blogs from a single manifest."""
2
+
3
+ __version__ = "0.4.3"
@@ -0,0 +1,7 @@
1
+ """`python -m presentation_sanity` — the same CLI, for environments without the script."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,89 @@
1
+ /* GENERATED by presentation-sanity — DO NOT EDIT.
2
+ * Defaults for \cite{} labels and the \bibliography list. The subject's own
3
+ * blog.css is imported after this file, so anything here can be overridden
4
+ * there without touching the generated theme. */
5
+
6
+ /* ─── In-text citations ──────────────────────────────────────────────────── */
7
+ .vp-doc a.cite {
8
+ font-variant-numeric: tabular-nums;
9
+ text-decoration: none;
10
+ font-weight: 500;
11
+ white-space: nowrap;
12
+ }
13
+ .vp-doc a.cite:hover {
14
+ text-decoration: underline;
15
+ }
16
+ .cite--missing {
17
+ background: rgba(255, 80, 80, 0.16);
18
+ color: #c00;
19
+ border-radius: 2px;
20
+ padding: 0 0.2em;
21
+ }
22
+
23
+ /* ─── Reference list ─────────────────────────────────────────────────────── */
24
+ .psanity-bibliography {
25
+ margin-top: 3.5rem;
26
+ border-top: 1px solid var(--vp-c-divider);
27
+ padding-top: 0.5rem;
28
+ }
29
+ .vp-doc .psanity-bibliography > :first-child {
30
+ margin-top: 1.2rem;
31
+ }
32
+ .vp-doc .psanity-bibliography ol.ref-list {
33
+ list-style: none;
34
+ padding-left: 0;
35
+ margin: 0;
36
+ }
37
+
38
+ /*
39
+ * Hanging indent: the marker sits in the gutter and continuation lines align
40
+ * under the reference text rather than under the marker.
41
+ *
42
+ * Inline flow is deliberate. `display: flex` or `grid` here looks like the
43
+ * obvious way to put the number beside the text, but a reference is a run of
44
+ * inline nodes — <em>journal</em>, <strong>volume</strong>, bare text, the DOI
45
+ * link — and each one becomes its own flex/grid item, so the entry renders as
46
+ * a row of narrow columns instead of a paragraph.
47
+ */
48
+ .vp-doc .psanity-bibliography li.ref {
49
+ padding-left: 2.5em;
50
+ text-indent: -2.5em;
51
+ margin: 0.55rem 0;
52
+ font-size: 0.9em;
53
+ line-height: 1.65;
54
+ color: var(--vp-c-text-2);
55
+ /* Jumping from a citation shouldn't tuck the entry under the navbar. */
56
+ scroll-margin-top: 6rem;
57
+ }
58
+ .vp-doc .psanity-bibliography li.ref + li.ref {
59
+ margin-top: 0.55rem;
60
+ }
61
+ .vp-doc .psanity-bibliography .ref-number {
62
+ display: inline-block;
63
+ width: 2.5em; /* matches the indent, so text lines up exactly */
64
+ text-indent: 0; /* don't inherit the negative indent */
65
+ font-variant-numeric: tabular-nums;
66
+ color: var(--vp-c-text-3);
67
+ }
68
+ .vp-doc .psanity-bibliography li.ref em {
69
+ color: var(--vp-c-text-1);
70
+ }
71
+ .vp-doc .psanity-bibliography a {
72
+ text-decoration: none;
73
+ font-weight: 400;
74
+ }
75
+ .vp-doc .psanity-bibliography a:hover {
76
+ text-decoration: underline;
77
+ }
78
+ .vp-doc .psanity-bibliography a.ref-link {
79
+ font-size: 0.95em;
80
+ overflow-wrap: anywhere; /* long DOIs shouldn't overflow the column */
81
+ }
82
+
83
+ /* Briefly mark the entry you jumped to. box-shadow rather than padding, so
84
+ * highlighting doesn't shift the hanging indent. */
85
+ .vp-doc .psanity-bibliography li.ref:target {
86
+ background: var(--vp-c-default-soft);
87
+ border-radius: 3px;
88
+ box-shadow: 0 0 0 0.45rem var(--vp-c-default-soft);
89
+ }
@@ -0,0 +1,277 @@
1
+ // GENERATED by presentation-sanity — DO NOT EDIT.
2
+ // Rewritten from manifest.yaml on every `presentation-sanity build|dev`.
3
+ // Change the blog's title, nav, base, math macros, etc. under `outputs.<key>:`
4
+ // and `math:` in manifest.yaml. To take full ownership instead, copy this
5
+ // directory elsewhere and point VitePress at it yourself.
6
+ import { defineConfig } from 'vitepress'
7
+ __IMPORTS__
8
+
9
+ // Everything derivable from manifest.yaml, injected as a literal.
10
+ const generated = __CONFIG_JSON__
11
+
12
+ // ─── Bare LaTeX display environments ────────────────────────────────────────
13
+ // markdown-it-mathjax3 only recognises `$…$` and `$$…$$`, so a document that
14
+ // opens with `\begin{align}` — the way you'd write it in a .tex file — falls
15
+ // through to the paragraph rule and renders as literal text with its `\\`
16
+ // row breaks eaten. This block rule consumes `\begin{env}…\end{env}` and emits
17
+ // the plugin's own `math_block` token, so such a block renders exactly as if
18
+ // it had been fenced in `$$`.
19
+ //
20
+ // Names from `math.environments` in the manifest; starred variants match too.
21
+ // In-math-only environments (matrix, cases, …) are deliberately absent — at
22
+ // block level they would swallow the prose that follows.
23
+ const MATH_ENVIRONMENTS: string[] = __ENVIRONMENTS__
24
+
25
+ function installLatexEnvironments(md: any) {
26
+ if (!MATH_ENVIRONMENTS.length) return
27
+ const allowed = new Set(MATH_ENVIRONMENTS.map((e) => e.replace(/\*$/, '')))
28
+
29
+ function latexEnvironment(state: any, startLine: number, endLine: number, silent: boolean) {
30
+ // Four-space indent is a code block, not math.
31
+ if (state.sCount[startLine] - state.blkIndent >= 4) return false
32
+
33
+ const begin = state.bMarks[startLine] + state.tShift[startLine]
34
+ const opening = /^\\begin\{([A-Za-z]+\*?)\}/.exec(
35
+ state.src.slice(begin, state.eMarks[startLine]),
36
+ )
37
+ if (!opening) return false
38
+
39
+ const env = opening[1]
40
+ if (!allowed.has(env.replace(/\*$/, ''))) return false
41
+ if (silent) return true
42
+
43
+ // Scan for the matching \end{env}, counting nested opens of the same
44
+ // environment so `align` inside `align` closes at the right line.
45
+ const closing = `\\end{${env}}`
46
+ const reopen = `\\begin{${env}}`
47
+ let depth = 0
48
+ let line = startLine
49
+ let found = false
50
+ for (; line < endLine; line++) {
51
+ const text = state.src.slice(
52
+ state.bMarks[line] + state.tShift[line],
53
+ state.eMarks[line],
54
+ )
55
+ if (line > startLine && text.includes(reopen)) depth++
56
+ if (text.includes(closing)) {
57
+ if (depth === 0) { found = true; break }
58
+ depth--
59
+ }
60
+ }
61
+ // Unterminated: leave it to the paragraph rule rather than eating the
62
+ // rest of the document.
63
+ if (!found) return false
64
+
65
+ const token = state.push('math_block', 'math', 0)
66
+ token.block = true
67
+ token.content = state.getLines(startLine, line + 1, state.blkIndent, false)
68
+ token.map = [startLine, line + 1]
69
+ token.markup = '$$'
70
+ state.line = line + 1
71
+ return true
72
+ }
73
+
74
+ md.block.ruler.before('paragraph', 'latex_environment', latexEnvironment, {
75
+ alt: ['paragraph', 'reference', 'blockquote', 'list'],
76
+ })
77
+ }
78
+
79
+ // ─── Citations ──────────────────────────────────────────────────────────────
80
+ // LaTeX-shaped citing without a LaTeX toolchain. `\cite{key}` becomes a linked
81
+ // label, `\bibliography` renders the reference list. Entries were parsed and
82
+ // formatted from the manifest's .bib sources at scaffold time and injected
83
+ // below, so nothing about citations reaches the browser — the page ships plain
84
+ // anchors, the same bargain the math rendering makes.
85
+ type BibEntry = {
86
+ html: string
87
+ text: string
88
+ label: string
89
+ labelBare: string
90
+ year: string
91
+ sortAuthor: string
92
+ sortYear: string
93
+ }
94
+ type BibData = {
95
+ style: 'numeric' | 'author-year'
96
+ sort: 'appearance' | 'author' | 'year'
97
+ title: string
98
+ heading: string
99
+ entries: Record<string, BibEntry>
100
+ }
101
+
102
+ const BIBLIOGRAPHY: BibData | null = __BIBLIOGRAPHY__
103
+
104
+ const CITE_COMMANDS: Record<string, 'p' | 't' | 'no'> = {
105
+ cite: 'p',
106
+ citep: 'p',
107
+ citealp: 'p',
108
+ citet: 't',
109
+ citealt: 't',
110
+ nocite: 'no',
111
+ }
112
+
113
+ function escapeHtml(s: string) {
114
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;')
115
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;')
116
+ }
117
+
118
+ function installCitations(md: any) {
119
+ const bib = BIBLIOGRAPHY
120
+ if (!bib) return
121
+ const warned = new Set<string>()
122
+
123
+ // \cite{a,b} · \citep[p. 12]{a} · \citet{a} · \nocite{a}
124
+ function citeRule(state: any, silent: boolean) {
125
+ if (state.src.charCodeAt(state.pos) !== 0x5c /* \ */) return false
126
+ const re = /^\\(cite|citep|citealp|citet|citealt|nocite)\s*(?:\[([^\]]*)\])?\{([^}]*)\}/
127
+ const m = re.exec(state.src.slice(state.pos))
128
+ if (!m) return false
129
+ const keys = m[3].split(',').map((k: string) => k.trim()).filter(Boolean)
130
+ if (!keys.length) return false
131
+ if (!silent) {
132
+ const token = state.push('psanity_cite', '', 0)
133
+ token.meta = { keys, form: CITE_COMMANDS[m[1]], note: (m[2] || '').trim() }
134
+ }
135
+ state.pos += m[0].length
136
+ return true
137
+ }
138
+
139
+ // A line that is only \bibliography, \bibliography{...} or \printbibliography
140
+ function bibliographyRule(state: any, startLine: number, _end: number, silent: boolean) {
141
+ if (state.sCount[startLine] - state.blkIndent >= 4) return false
142
+ const line = state.src
143
+ .slice(state.bMarks[startLine] + state.tShift[startLine], state.eMarks[startLine])
144
+ .trim()
145
+ if (!/^\\(?:print)?bibliography(\{[^}]*\})?$/.test(line)) return false
146
+ if (silent) return true
147
+ state.push('psanity_bibliography', '', 0)
148
+ state.line = startLine + 1
149
+ return true
150
+ }
151
+
152
+ // Collect every cited key, then fix numbering and list order in one pass —
153
+ // so `sort: author` can still produce ascending [1],[2] in the text.
154
+ function collectRule(state: any) {
155
+ const seen: string[] = []
156
+ const visit = (tokens: any[]) => {
157
+ for (const t of tokens) {
158
+ if (t.type === 'psanity_cite') {
159
+ for (const k of t.meta.keys) if (!seen.includes(k)) seen.push(k)
160
+ }
161
+ if (t.children) visit(t.children)
162
+ }
163
+ }
164
+ visit(state.tokens)
165
+
166
+ const known = seen.filter((k) => bib.entries[k])
167
+ let order = known.slice()
168
+ if (bib.sort === 'author') {
169
+ order.sort((a, b) => bib.entries[a].sortAuthor.localeCompare(bib.entries[b].sortAuthor))
170
+ } else if (bib.sort === 'year') {
171
+ order.sort((a, b) => bib.entries[a].sortYear.localeCompare(bib.entries[b].sortYear))
172
+ }
173
+ const numbers: Record<string, number> = {}
174
+ order.forEach((k, i) => { numbers[k] = i + 1 })
175
+ state.env.psanityBib = { order, numbers }
176
+ }
177
+
178
+ // BEFORE 'escape', not after: markdown-it's escape rule consumes `\X` for
179
+ // *any* X — including `\c` — and returns true, so a rule registered after it
180
+ // never sees `\cite`. Running first is safe because this rule only matches a
181
+ // backslash followed by a known citation command, so `\$100` and friends
182
+ // still fall through to escape, and `$…$` still reaches the math rule.
183
+ md.inline.ruler.before('escape', 'psanity_cite', citeRule)
184
+ md.block.ruler.before('paragraph', 'psanity_bibliography', bibliographyRule, {
185
+ alt: ['paragraph', 'reference', 'blockquote', 'list'],
186
+ })
187
+ md.core.ruler.push('psanity_collect_citations', collectRule)
188
+
189
+ md.renderer.rules.psanity_cite = (tokens: any[], idx: number, _o: any, env: any) => {
190
+ const { keys, form, note } = tokens[idx].meta
191
+ if (form === 'no') return ''
192
+ const numbers = env?.psanityBib?.numbers ?? {}
193
+ const parts = keys.map((key: string) => {
194
+ const entry = bib.entries[key]
195
+ if (!entry) {
196
+ if (!warned.has(key)) {
197
+ warned.add(key)
198
+ console.warn(` warning: \\cite{${key}} — no such key in the bibliography`)
199
+ }
200
+ return `<span class="cite cite--missing">?${escapeHtml(key)}</span>`
201
+ }
202
+ const href = `#ref-${encodeURIComponent(key)}`
203
+ const title = escapeHtml(entry.text)
204
+ if (bib.style === 'author-year') {
205
+ const inner = form === 't' ? entry.labelBare : entry.label
206
+ return `<a class="cite" href="${href}" title="${title}">${escapeHtml(inner)}</a>`
207
+ }
208
+ return `<a class="cite" href="${href}" title="${title}">${numbers[key]}</a>`
209
+ })
210
+
211
+ // The locator comes straight from markdown, so apply the TeX dash
212
+ // conventions the .bib fields already get: `pp. 401--403` → 401–403.
213
+ const suffix = note
214
+ ? `, ${escapeHtml(note.replace(/---/g, '\u2014').replace(/--/g, '\u2013'))}`
215
+ : ''
216
+ if (bib.style === 'author-year') {
217
+ // \citet{k} → Smith et al. (2020); \citep{k} → (Smith et al., 2020)
218
+ if (form === 't' && keys.length === 1) {
219
+ const entry = bib.entries[keys[0]]
220
+ const year = entry ? escapeHtml(entry.year) : ''
221
+ return `${parts[0]} (${year}${suffix})`
222
+ }
223
+ return `(${parts.join('; ')}${suffix})`
224
+ }
225
+ const body = `[${parts.join(', ')}${suffix}]`
226
+ return form === 't' && keys.length === 1 && bib.entries[keys[0]]
227
+ ? `${escapeHtml(bib.entries[keys[0]].labelBare)} ${body}`
228
+ : body
229
+ }
230
+
231
+ md.renderer.rules.psanity_bibliography = (_t: any, _i: number, _o: any, env: any) => {
232
+ const order: string[] = env?.psanityBib?.order ?? []
233
+ if (!order.length) return ''
234
+ const numbered = bib.style === 'numeric'
235
+ const items = order
236
+ .map((key) => {
237
+ const e = bib.entries[key]
238
+ // No trailing space: .ref-number is a fixed-width inline-block, so the
239
+ // gap is the CSS width and the text aligns with the hanging indent.
240
+ const n = numbered ? `<span class="ref-number">[${env.psanityBib.numbers[key]}]</span>` : ''
241
+ return `<li class="ref" id="ref-${encodeURIComponent(key)}">${n}${e.html}</li>`
242
+ })
243
+ .join('\n')
244
+ const h = /^h[1-6]$/.test(bib.heading) ? bib.heading : 'h2'
245
+ const title = bib.title
246
+ ? `<${h} id="${encodeURIComponent(bib.title.toLowerCase().replace(/\s+/g, '-'))}">${escapeHtml(bib.title)}</${h}>`
247
+ : ''
248
+ return `<div class="psanity-bibliography">\n${title}\n<ol class="ref-list">\n${items}\n</ol>\n</div>\n`
249
+ }
250
+ }
251
+
252
+ export default defineConfig({
253
+ ...generated,
254
+ markdown: {
255
+ ...generated.markdown,
256
+ // Runs after VitePress installs markdown-it-mathjax3, so the `math_block`
257
+ // renderer the rule above emits into is already registered.
258
+ config: (md: any) => {
259
+ if (generated.markdown?.math) installLatexEnvironments(md)
260
+ installCitations(md)
261
+ },
262
+ },
263
+ vite: {
264
+ // `configFile: false` is load-bearing. The subject root also holds a
265
+ // `vite.config.ts` for Slidev, and VitePress would otherwise merge it in:
266
+ // its `base: './'` is invalid for VitePress routing, and its yaml plugin
267
+ // would transform manifest.yaml a SECOND time — the double pass re-parses
268
+ // the emitted JS as YAML and hands every component a string instead of the
269
+ // manifest. This config is self-contained; the deck's is left to the deck.
270
+ configFile: false,
271
+ plugins: [__VITE_PLUGINS__],
272
+ // In a project: `@project` → the project root, `@shared` → its shared/ Vue
273
+ // layer — the same aliases the deck's shared/vite.config.ts defines, so a
274
+ // component imports `@project/manifest.yaml` identically in both engines.
275
+ resolve: { alias: __ALIASES__ },
276
+ },
277
+ })
@@ -0,0 +1,43 @@
1
+ // GENERATED by presentation-sanity — DO NOT EDIT.
2
+ //
3
+ // Wires the shared Vue components into VitePress so the same components back
4
+ // both the Slidev deck and the blog:
5
+ //
6
+ // * every component file is registered globally under its filename, which
7
+ // is exactly the convention Slidev already uses. `<DataValue>`,
8
+ // `<ManimFigure>`, `<FigureImage>` therefore work in markdown with no
9
+ // per-file imports.
10
+ // * `ProvenancePanel` is mounted once in the `layout-bottom` slot — the
11
+ // VitePress equivalent of Slidev's `global-bottom.vue` singleton.
12
+ import { h } from 'vue'
13
+ import type { Theme } from 'vitepress'
14
+ import DefaultTheme from 'vitepress/theme'
15
+ __THEME_IMPORTS__
16
+
17
+ // Literal globs (Vite requires static patterns), in priority order: a later
18
+ // directory overrides an earlier one, so a presentation's own component wins
19
+ // over the shared component of the same name.
20
+ const sources = [
21
+ __COMPONENT_GLOBS__
22
+ ]
23
+
24
+ const components: Record<string, any> = {}
25
+ for (const modules of sources) {
26
+ for (const [path, mod] of Object.entries(modules)) {
27
+ const name = path.split('/').pop()!.replace(/\.vue$/, '')
28
+ if (mod?.default) components[name] = mod.default
29
+ }
30
+ }
31
+
32
+ export default {
33
+ extends: DefaultTheme,
34
+ Layout() {
35
+ const panel = components.ProvenancePanel
36
+ return h(DefaultTheme.Layout, null, panel ? { 'layout-bottom': () => h(panel) } : {})
37
+ },
38
+ enhanceApp({ app }) {
39
+ for (const [name, component] of Object.entries(components)) {
40
+ app.component(name, component)
41
+ }
42
+ },
43
+ } satisfies Theme