@panaversity/ksor 0.0.30 → 0.0.31

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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Turning a presentation's share link into the url that can be framed.
3
+ *
4
+ * Every deck host publishes two different urls — the one you get from "Share"
5
+ * and the one that works in an `<iframe>` — and an author will paste the first.
6
+ * Deriving the second is a small rule per provider, and getting it wrong is
7
+ * invisible until someone opens the page, so it is a table with tests rather
8
+ * than a regex written once.
9
+ *
10
+ * A LEAF: no imports. `slides.ts` carries zod and cannot enter the unit tier.
11
+ */
12
+
13
+ /**
14
+ * Only https. A browser blocks an http frame inside a secure page as mixed
15
+ * content, so an http embed publishes a panel that silently never loads —
16
+ * which is worse than refusing it, because nothing goes red.
17
+ */
18
+ export function isHttpsUrl(value: string): boolean {
19
+ try {
20
+ return new URL(value).protocol === "https:";
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * One provider's rule: which hosts it owns, and how a share url becomes an
28
+ * embed url. Extend this rather than special-casing at a call site.
29
+ */
30
+ interface Provider {
31
+ readonly label: string;
32
+ readonly hosts: readonly string[];
33
+ readonly embed: (url: URL) => string | null;
34
+ }
35
+
36
+ const PROVIDERS: readonly Provider[] = [
37
+ {
38
+ label: "Google Slides",
39
+ hosts: ["docs.google.com"],
40
+ // /presentation/d/<id>/edit -> /presentation/d/<id>/embed
41
+ embed: (url) => {
42
+ const match = /^\/presentation\/d\/([^/]+)/.exec(url.pathname);
43
+ if (match === null) return null;
44
+ return `https://docs.google.com/presentation/d/${match[1]}/embed?start=false&loop=false&delayms=3000`;
45
+ },
46
+ },
47
+ {
48
+ label: "Canva",
49
+ hosts: ["www.canva.com", "canva.com"],
50
+ // /design/<id>/<token>/view -> same, with ?embed
51
+ embed: (url) => {
52
+ const match = /^\/design\/([^/]+)\/([^/]+)/.exec(url.pathname);
53
+ if (match === null) return null;
54
+ return `https://www.canva.com/design/${match[1]}/${match[2]}/view?embed`;
55
+ },
56
+ },
57
+ {
58
+ label: "SlideShare",
59
+ hosts: ["www.slideshare.net", "slideshare.net"],
60
+ // Only an explicit /slideshow/embed_code/ url is framable.
61
+ embed: (url) => (url.pathname.startsWith("/slideshow/embed_code/") ? url.toString() : null),
62
+ },
63
+ ];
64
+
65
+ /** The provider's own name for a url, or null when it is not one we know. */
66
+ export function providerOf(value: string): string | null {
67
+ try {
68
+ const url = new URL(value);
69
+ return PROVIDERS.find((p) => p.hosts.includes(url.hostname))?.label ?? null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * The url to put in the frame, or null when this one cannot be derived.
77
+ *
78
+ * Null is an ordinary answer, not an error: the deck still renders as a link,
79
+ * and an author who wants a frame supplies `embed:` explicitly.
80
+ */
81
+ export function embedUrlFor(value: string): string | null {
82
+ try {
83
+ const url = new URL(value);
84
+ if (url.protocol !== "https:") return null;
85
+ const provider = PROVIDERS.find((p) => p.hosts.includes(url.hostname));
86
+ return provider?.embed(url) ?? null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /** Every provider whose share url this understands, for docs and errors. */
93
+ export const KNOWN_PROVIDERS: readonly string[] = PROVIDERS.map((p) => p.label);
@@ -0,0 +1,123 @@
1
+ import { z } from "zod";
2
+
3
+ import { embedUrlFor, isHttpsUrl } from "./slides-embed";
4
+
5
+ /**
6
+ * The shape of a `<doc>.slides.yaml` — a presentation that teaches this
7
+ * document.
8
+ *
9
+ * The predecessor authors this INLINE in MDX: a `## 📚 Teaching Aid` heading,
10
+ * a `:::tip` with the link, and a raw `<div style={{…}}>` wrapping an
11
+ * `<iframe>` (`specs/crashcourses/connector-native-apps/…md`). None of that is
12
+ * available here and the reason is critical rule 2: `knowledge/` is CommonMark
13
+ * only, so a document cannot carry raw JSX and stay readable in a plain
14
+ * markdown viewer. The deck is an attachment instead, which also means it
15
+ * inherits the document's tier and takedown rather than being an embed nobody
16
+ * governs.
17
+ */
18
+ /**
19
+ * One slide, as the record carries it.
20
+ *
21
+ * Deliberately NOT freeform markdown or HTML. A slide is a heading and a few
22
+ * lines, and admitting arbitrary markup would put layout into the record —
23
+ * which is the same mistake as embedding a deck, one level down. What a slide
24
+ * says is knowledge; how it looks is the site's business.
25
+ */
26
+ export const SlideSchema = z.object({
27
+ heading: z.string().min(1).max(120),
28
+ /**
29
+ * Three to five in practice. The cap is six because a slide someone reads
30
+ * aloud is a slide nobody listens to, and a limit is the only thing that
31
+ * reliably stops a generator from pasting a paragraph per slide.
32
+ */
33
+ bullets: z.array(z.string().min(1).max(240)).max(6).optional(),
34
+ /** One line under the heading, for a slide that makes a single point. */
35
+ lead: z.string().max(300).optional(),
36
+ /** Spoken, not shown. Rendered for the presenter, never on the slide. */
37
+ note: z.string().max(600).optional(),
38
+ });
39
+
40
+ export const SlidesSchema = z
41
+ .object({
42
+ slides: z.object({
43
+ title: z.string().min(1).max(120),
44
+ /**
45
+ * Where the deck lives. HTTPS only — an http:// embed is blocked as
46
+ * mixed content on any deployed site, so accepting one would publish a
47
+ * frame that silently never loads.
48
+ */
49
+ url: z.string().url().max(2000).optional(),
50
+ /**
51
+ * The provider's EMBED url, when it differs from the share url. Optional
52
+ * because `embedUrlFor` derives it for providers whose rule is known;
53
+ * required in practice for any provider whose is not, and a deck with
54
+ * neither renders as a link rather than as a broken frame.
55
+ */
56
+ embed: z.string().url().max(2000).optional(),
57
+ /** Shown beside the link — "Google Slides", "Canva". Never inferred. */
58
+ provider: z.string().max(60).optional(),
59
+ /** One line under the title, if the deck needs introducing. */
60
+ description: z.string().max(300).optional(),
61
+ }),
62
+ /**
63
+ * The slides themselves, when the record carries them.
64
+ *
65
+ * This is the mode that makes the workflow complete: an agent writes these
66
+ * from the document with no browser and no third party, and the site
67
+ * renders the presentation. The deck is then governed like everything else
68
+ * here — reviewed in a PR, versioned with its document, withdrawn with it —
69
+ * and it cannot rot into a dead link, because there is no link.
70
+ */
71
+ deck: z.array(SlideSchema).min(1).max(60).optional(),
72
+ })
73
+ .superRefine((value, ctx) => {
74
+ // Exactly one source. A deck that is BOTH authored here and embedded from
75
+ // elsewhere has two versions and no answer to which one governs — which is
76
+ // the disagreement this whole product exists to settle.
77
+ const authored = (value.deck?.length ?? 0) > 0;
78
+ const linked = value.slides.url !== undefined;
79
+ if (!authored && !linked) {
80
+ ctx.addIssue({
81
+ code: "custom",
82
+ path: ["deck"],
83
+ message:
84
+ "ksor-slides-empty: a presentation needs either `deck:` (slides the record carries) or `slides.url:` (a deck hosted elsewhere) — with neither there is nothing to show",
85
+ });
86
+ }
87
+ if (authored && linked) {
88
+ ctx.addIssue({
89
+ code: "custom",
90
+ path: ["deck"],
91
+ message:
92
+ "ksor-slides-two-sources: this declares both `deck:` and `slides.url:`, so there are two presentations and nothing says which one governs — keep the one the record owns, or drop `deck:` and keep the link",
93
+ });
94
+ }
95
+
96
+ for (const key of ["url", "embed"] as const) {
97
+ const candidate = value.slides[key];
98
+ if (candidate !== undefined && !isHttpsUrl(candidate)) {
99
+ ctx.addIssue({
100
+ code: "custom",
101
+ path: ["slides", key],
102
+ message: `ksor-slides-insecure: ${key} must be https:// — a browser blocks an http:// frame on a secure page as mixed content, so this would publish a deck that silently never loads`,
103
+ });
104
+ }
105
+ }
106
+ // A deck nobody can embed is still a legitimate deck — it renders as a
107
+ // link. But a deck we cannot embed AND whose provider we cannot name is
108
+ // worth telling the author about, because they probably expected a frame.
109
+ if (
110
+ linked &&
111
+ value.slides.embed === undefined &&
112
+ embedUrlFor(value.slides.url ?? "") === null
113
+ ) {
114
+ ctx.addIssue({
115
+ code: "custom",
116
+ path: ["slides", "embed"],
117
+ message: `ksor-slides-no-embed: this url has no embed form ksor knows how to derive, so the deck would render as a link only — add an explicit \`embed:\` url if you want it shown inline`,
118
+ });
119
+ }
120
+ });
121
+
122
+ export type Slides = z.infer<typeof SlidesSchema>;
123
+ export type Slide = z.infer<typeof SlideSchema>;
@@ -3,6 +3,7 @@ import { metaSchema, pageSchema } from "fumadocs-core/source/schema";
3
3
  import { z } from "zod";
4
4
  import { DeckSchema } from "./lib/deck";
5
5
  import { QuizSchema } from "./lib/quiz";
6
+ import { SlidesSchema } from "./lib/slides";
6
7
  import { knowledgeSourceDir } from "./lib/stage-knowledge";
7
8
 
8
9
  // The record lives at <repo>/knowledge — two levels up from this site.
@@ -103,6 +104,14 @@ export const quizzes = defineCollections({
103
104
  schema: QuizSchema,
104
105
  });
105
106
 
107
+ /** The presentation that teaches a document — see components/slides.tsx. */
108
+ export const slides = defineCollections({
109
+ type: "meta",
110
+ dir: knowledgeSourceDir(),
111
+ files: ["**/*.slides.yaml"],
112
+ schema: SlidesSchema,
113
+ });
114
+
106
115
  export default defineConfig({
107
116
  mdxOptions: {
108
117
  // MDX options