@immediately-run/sdk 0.30.0 → 0.31.0

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 (41) hide show
  1. package/dist/boot.cjs +5 -1
  2. package/dist/boot.cjs.map +1 -1
  3. package/dist/boot.js +5 -1
  4. package/dist/boot.js.map +1 -1
  5. package/dist/components/Link.cjs +25 -0
  6. package/dist/components/Link.cjs.map +1 -1
  7. package/dist/components/Link.d.cts +7 -1
  8. package/dist/components/Link.d.ts +7 -1
  9. package/dist/components/Link.js +24 -0
  10. package/dist/components/Link.js.map +1 -1
  11. package/dist/components/ScrollAfterNavigation.cjs +66 -0
  12. package/dist/components/ScrollAfterNavigation.cjs.map +1 -0
  13. package/dist/components/ScrollAfterNavigation.d.cts +24 -0
  14. package/dist/components/ScrollAfterNavigation.d.ts +24 -0
  15. package/dist/components/ScrollAfterNavigation.js +41 -0
  16. package/dist/components/ScrollAfterNavigation.js.map +1 -0
  17. package/dist/components/WikiLink.cjs +10 -2
  18. package/dist/components/WikiLink.cjs.map +1 -1
  19. package/dist/components/WikiLink.d.cts +11 -3
  20. package/dist/components/WikiLink.d.ts +11 -3
  21. package/dist/components/WikiLink.js +10 -2
  22. package/dist/components/WikiLink.js.map +1 -1
  23. package/dist/scrollToId.cjs +46 -0
  24. package/dist/scrollToId.cjs.map +1 -0
  25. package/dist/scrollToId.d.cts +12 -0
  26. package/dist/scrollToId.d.ts +12 -0
  27. package/dist/scrollToId.js +22 -0
  28. package/dist/scrollToId.js.map +1 -0
  29. package/dist/urlUtils.cjs +9 -1
  30. package/dist/urlUtils.cjs.map +1 -1
  31. package/dist/urlUtils.d.cts +10 -1
  32. package/dist/urlUtils.d.ts +10 -1
  33. package/dist/urlUtils.js +8 -1
  34. package/dist/urlUtils.js.map +1 -1
  35. package/dist/version.cjs +1 -1
  36. package/dist/version.cjs.map +1 -1
  37. package/dist/version.d.cts +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/dist/version.js.map +1 -1
  41. package/package.json +1 -1
package/dist/boot.cjs CHANGED
@@ -33,6 +33,7 @@ var import_errors = require("./components/errors");
33
33
  var import_FileRouter = require("./components/FileRouter");
34
34
  var import_MainContent = require("./components/MainContent");
35
35
  var import_MDXComponents = require("./components/MDXComponents");
36
+ var import_ScrollAfterNavigation = require("./components/ScrollAfterNavigation");
36
37
  var import_contextUtils = require("./contextUtils");
37
38
  var import_injectedBundler = require("./injectedBundler");
38
39
  var import_MDXProvider = require("./MDXProvider");
@@ -90,7 +91,10 @@ const TinkerableApp = ({
90
91
  source.enable();
91
92
  return dispose;
92
93
  }, [setContext]);
93
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TinkerableContext.TinkerableContext, { value: context, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_routing.Router, {}) });
94
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_TinkerableContext.TinkerableContext, { value: context, children: [
95
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ScrollAfterNavigation.ScrollAfterNavigation, {}),
96
+ children ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_routing.Router, {})
97
+ ] });
94
98
  };
95
99
  const BootMarkers = () => {
96
100
  (0, import_react.useLayoutEffect)(() => {
package/dist/boot.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/boot.tsx"],"sourcesContent":["import { FC, ReactNode, StrictMode, useEffect, useLayoutEffect, useState } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport { emitMarkerOnce } from './markers';\n\nimport { ErrorNotFound } from './components/errors';\nimport { FileRouter } from './components/FileRouter';\nimport { MainContent } from './components/MainContent';\nimport { DEFAULT_MDX_COMPONENTS } from './components/MDXComponents';\nimport { getInitialContext, updateContext } from './contextUtils';\nimport { getInjectedMetadataEmitter, resolveMetadataSource } from './injectedBundler';\nimport { MDXProvider } from './MDXProvider';\nimport { ModuleCache, ModuleCacheContextProvider } from './moduleCache';\nimport { Router } from './routing';\nimport type { RoutingSpec } from './RoutingSpec';\nimport { FilesMetadata } from './sandboxTypes';\nimport { addListener } from './sandboxUtils';\nimport { TinkerableContext, TinkerableState } from './TinkerableContext';\nimport { FILES_PREFIX } from './urlUtils';\n\n/** A map of MDX component overrides, or a function that receives the platform\n * {@link DEFAULT_MDX_COMPONENTS} and returns the full map to use. */\nexport type MdxComponents =\n | Record<string, FC>\n | ((defaults: Record<string, FC>) => Record<string, FC>);\n\n/**\n * Resolve the effective MDX component map from a {@link BootProps.mdxComponents}\n * value (MARKDOWN_SYNTAX_SPEC §11.3):\n * - `undefined` → the platform {@link DEFAULT_MDX_COMPONENTS} (same reference).\n * - a **function** → the full-replace escape hatch, handed the defaults.\n * - a **map** → merged *over* the defaults (`{ ...defaults, ...map }`), so\n * overriding one component keeps the rest — the phantom-defaults invariant\n * (§11.2) that stops the MDX missing-reference guard from firing.\n */\nexport const resolveMdxComponents = (mdxComponents?: MdxComponents): Record<string, FC> =>\n mdxComponents === undefined\n ? (DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : typeof mdxComponents === 'function'\n ? mdxComponents(DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : { ...(DEFAULT_MDX_COMPONENTS as Record<string, FC>), ...mdxComponents };\n\n/** Options for {@link boot}: MDX overrides, a route table, or an app root. */\nexport type BootProps = {\n /**\n * MDX component overrides. A **map** is *merged over* the platform defaults\n * ({@link DEFAULT_MDX_COMPONENTS}) — so overriding `WikiLink` alone keeps the\n * default `a` and `Admonition` (MARKDOWN_SYNTAX_SPEC §11.3). Pass a **function**\n * `(defaults) => map` as the full-replace escape hatch when you want complete\n * control over the set.\n */\n mdxComponents?: MdxComponents;\n routingSpec?: RoutingSpec;\n /**\n * App root rendered directly inside the providers (with full navigation\n * context), instead of dispatching through a `routingSpec`. Render\n * `<Routes>`/`<Route>` here for fully dynamic routing — no catch-all rule\n * boilerplate. Takes precedence over `routingSpec` for what is rendered.\n */\n children?: ReactNode;\n};\n\nconst updateAlreadyApplied = (filesMetadata: FilesMetadata, update: FilesMetadata) => {\n for (let [key, value] of Object.entries(update)) {\n if (filesMetadata[key] !== value) {\n return false;\n }\n }\n return true;\n};\n\n/** The app shell {@link boot} renders: holds navigation state, subscribes to host\n * URL + metadata pushes, and renders `children` or the route `<Router />`. */\nexport const TinkerableApp = ({\n routingSpec,\n children,\n}: {\n routingSpec: RoutingSpec;\n children?: ReactNode;\n}) => {\n const [context, setContext] = useState<TinkerableState>(getInitialContext(routingSpec));\n useEffect(() => {\n const removeListener = addListener('urlchange', ({ url }) => {\n setContext((context) => {\n const updatedContext = updateContext(context, url);\n if (updatedContext !== context) {\n console.log(\n `[Sandbox] Updating path from ${context.navigationState.sandboxPath} to ${updatedContext.navigationState.sandboxPath}`\n );\n }\n return updatedContext;\n });\n });\n return removeListener;\n }, [setContext]);\n useEffect(() => {\n // Phase 5 dual-mode (SDK_PACKAGING_SPEC §4/§8): prefer the injected bundler's\n // metadata emitter (the live path, byte-identical); when the SDK is npm-fetched\n // with no injection, `event` is undefined so `addListener` receives\n // 'metadata-update' over the §4 transport instead, and `enable` is a no-op.\n const source = resolveMetadataSource(getInjectedMetadataEmitter());\n const dispose = addListener(\n 'metadata-update',\n ({ update }: Record<string, any>) => {\n setContext((prevContext) =>\n updateAlreadyApplied(prevContext.filesMetadata, update)\n ? prevContext\n : {\n ...prevContext,\n filesMetadata: {\n // TODO: file deletion!\n ...prevContext.filesMetadata,\n ...update,\n },\n }\n );\n },\n source.event\n );\n source.enable();\n return dispose;\n }, [setContext]);\n\n return (\n <TinkerableContext value={context}>\n {children ?? <Router />}\n </TinkerableContext>\n );\n};\n\n// Boot marker emitter (LOAD_PROFILING_SPEC §3, R3-46). Rendered at the top of the\n// app tree so its layout effect fires on the FIRST root-render commit: that instant\n// is `ir.fmp` (the content is in the DOM, about to paint) and the baseline for\n// `ir.interactive` (the host treats a forwarded `ir.interactive` as the root-commit\n// signal and resolves `max(commit, reportReady)` — LP2-3 — so this can only ever be\n// delayed by an app's `reportReady()`, never advanced). Emitted in canonical stream\n// order (fmp then interactive); idempotent per name (StrictMode-safe). Renders null.\nconst BootMarkers = (): null => {\n useLayoutEffect(() => {\n emitMarkerOnce('ir.fmp');\n emitMarkerOnce('ir.interactive');\n }, []);\n return null;\n};\n\n/** The default route table when `boot` is called with no `routingSpec`/`children`:\n * `/` → main content, `/files/<path>` → the file router, else not-found.\n * Re-expressed with path templates (SANDBOX_ROUTING_SPEC §7); the file path\n * surfaces under the `*` wildcard. The catch-all stays a raw RegExp — the\n * escape hatch — so it anchors `.+` (a non-empty path) exactly as before. */\nexport const DEFAULT_ROUTING_SPEC: RoutingSpec = {\n routes: [\n { name: 'MainContent', pattern: '/', element: <MainContent /> },\n { name: 'FileRouter', pattern: `${FILES_PREFIX}/*`, element: <FileRouter /> },\n { name: 'ErrorNotFound', pattern: /^(?<path>.+)$/, element: <ErrorNotFound /> },\n ],\n};\n\n/**\n * Matches any `sandboxPath` so navigation context can be built without a route\n * table. Used when {@link boot} is given `children` (the app owns dispatch via\n * `<Routes>`); the catch-all's `element` is never rendered (children are).\n */\nexport const CATCH_ALL_ROUTING_SPEC: RoutingSpec = {\n routes: [{ name: 'AppRoot', pattern: /^.*$/, element: null }],\n};\n\n/**\n * Mount an immediately.run app into the sandbox `#root`. The entry point every\n * app calls from `index.tsx`: wires the MDX, module-cache, and navigation\n * providers, then renders the route table (`routingSpec`) or your `children`.\n */\nexport const boot = ({\n mdxComponents,\n routingSpec,\n children,\n}: BootProps = {}) => {\n const rootElement = document.getElementById('root');\n if (!rootElement) {\n throw new Error('boot requires root HTML element to exist');\n }\n const resolvedComponents = resolveMdxComponents(mdxComponents);\n // `children` own dispatch, so a catch-all keeps navigation working without a\n // table; otherwise fall back to the default file/main-content routes.\n const spec = routingSpec ?? (children ? CATCH_ALL_ROUTING_SPEC : DEFAULT_ROUTING_SPEC);\n const moduleCache = new ModuleCache();\n const root = createRoot(rootElement);\n root.render(\n <StrictMode>\n <ModuleCacheContextProvider moduleCache={moduleCache}>\n <MDXProvider components={resolvedComponents}>\n <BootMarkers />\n <TinkerableApp routingSpec={spec}>{children}</TinkerableApp>\n </MDXProvider>\n </ModuleCacheContextProvider>\n </StrictMode>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6HmB;AA7HnB,mBAAgF;AAChF,oBAA2B;AAE3B,qBAA+B;AAE/B,oBAA8B;AAC9B,wBAA2B;AAC3B,yBAA4B;AAC5B,2BAAuC;AACvC,0BAAiD;AACjD,6BAAkE;AAClE,yBAA4B;AAC5B,yBAAwD;AACxD,qBAAuB;AAGvB,0BAA4B;AAC5B,+BAAmD;AACnD,sBAA6B;AAiBtB,MAAM,uBAAuB,CAAC,kBACnC,kBAAkB,SACb,8CACD,OAAO,kBAAkB,aACvB,cAAc,2CAA4C,IAC1D,EAAE,GAAI,6CAA+C,GAAG,cAAc;AAsB9E,MAAM,uBAAuB,CAAC,eAA8B,WAA0B;AACpF,WAAS,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC/C,QAAI,cAAc,GAAG,MAAM,OAAO;AAChC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAIO,MAAM,gBAAgB,CAAC;AAAA,EAC5B;AAAA,EACA;AACF,MAGM;AACJ,QAAM,CAAC,SAAS,UAAU,QAAI,2BAA0B,uCAAkB,WAAW,CAAC;AACtF,8BAAU,MAAM;AACd,UAAM,qBAAiB,iCAAY,aAAa,CAAC,EAAE,IAAI,MAAM;AAC3D,iBAAW,CAACA,aAAY;AACtB,cAAM,qBAAiB,mCAAcA,UAAS,GAAG;AACjD,YAAI,mBAAmBA,UAAS;AAC9B,kBAAQ;AAAA,YACN,gCAAgCA,SAAQ,gBAAgB,WAAW,OAAO,eAAe,gBAAgB,WAAW;AAAA,UACtH;AAAA,QACF;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH,CAAC;AACD,WAAO;AAAA,EACT,GAAG,CAAC,UAAU,CAAC;AACf,8BAAU,MAAM;AAKd,UAAM,aAAS,kDAAsB,mDAA2B,CAAC;AACjE,UAAM,cAAU;AAAA,MACd;AAAA,MACA,CAAC,EAAE,OAAO,MAA2B;AACnC;AAAA,UAAW,CAAC,gBACV,qBAAqB,YAAY,eAAe,MAAM,IAClD,cACA;AAAA,YACE,GAAG;AAAA,YACH,eAAe;AAAA;AAAA,cAEb,GAAG,YAAY;AAAA,cACf,GAAG;AAAA,YACL;AAAA,UACF;AAAA,QACN;AAAA,MACF;AAAA,MACA,OAAO;AAAA,IACT;AACA,WAAO,OAAO;AACd,WAAO;AAAA,EACT,GAAG,CAAC,UAAU,CAAC;AAEf,SACE,4CAAC,8CAAkB,OAAO,SACvB,sBAAY,4CAAC,yBAAO,GACvB;AAEJ;AASA,MAAM,cAAc,MAAY;AAC9B,oCAAgB,MAAM;AACpB,uCAAe,QAAQ;AACvB,uCAAe,gBAAgB;AAAA,EACjC,GAAG,CAAC,CAAC;AACL,SAAO;AACT;AAOO,MAAM,uBAAoC;AAAA,EAC/C,QAAQ;AAAA,IACN,EAAE,MAAM,eAAe,SAAS,KAAK,SAAS,4CAAC,kCAAY,EAAG;AAAA,IAC9D,EAAE,MAAM,cAAc,SAAS,GAAG,4BAAY,MAAM,SAAS,4CAAC,gCAAW,EAAG;AAAA,IAC5E,EAAE,MAAM,iBAAiB,SAAS,iBAAiB,SAAS,4CAAC,+BAAc,EAAG;AAAA,EAChF;AACF;AAOO,MAAM,yBAAsC;AAAA,EACjD,QAAQ,CAAC,EAAE,MAAM,WAAW,SAAS,QAAQ,SAAS,KAAK,CAAC;AAC9D;AAOO,MAAM,OAAO,CAAC;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AACF,IAAe,CAAC,MAAM;AACpB,QAAM,cAAc,SAAS,eAAe,MAAM;AAClD,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AACA,QAAM,qBAAqB,qBAAqB,aAAa;AAG7D,QAAM,OAAO,gBAAgB,WAAW,yBAAyB;AACjE,QAAM,cAAc,IAAI,+BAAY;AACpC,QAAM,WAAO,0BAAW,WAAW;AACnC,OAAK;AAAA,IACH,4CAAC,2BACC,sDAAC,iDAA2B,aAC1B,uDAAC,kCAAY,YAAY,oBACvB;AAAA,kDAAC,eAAY;AAAA,MACb,4CAAC,iBAAc,aAAa,MAAO,UAAS;AAAA,OAC9C,GACF,GACF;AAAA,EACF;AACF;","names":["context"]}
1
+ {"version":3,"sources":["../src/boot.tsx"],"sourcesContent":["import { FC, ReactNode, StrictMode, useEffect, useLayoutEffect, useState } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport { emitMarkerOnce } from './markers';\n\nimport { ErrorNotFound } from './components/errors';\nimport { FileRouter } from './components/FileRouter';\nimport { MainContent } from './components/MainContent';\nimport { DEFAULT_MDX_COMPONENTS } from './components/MDXComponents';\nimport { ScrollAfterNavigation } from './components/ScrollAfterNavigation';\nimport { getInitialContext, updateContext } from './contextUtils';\nimport { getInjectedMetadataEmitter, resolveMetadataSource } from './injectedBundler';\nimport { MDXProvider } from './MDXProvider';\nimport { ModuleCache, ModuleCacheContextProvider } from './moduleCache';\nimport { Router } from './routing';\nimport type { RoutingSpec } from './RoutingSpec';\nimport { FilesMetadata } from './sandboxTypes';\nimport { addListener } from './sandboxUtils';\nimport { TinkerableContext, TinkerableState } from './TinkerableContext';\nimport { FILES_PREFIX } from './urlUtils';\n\n/** A map of MDX component overrides, or a function that receives the platform\n * {@link DEFAULT_MDX_COMPONENTS} and returns the full map to use. */\nexport type MdxComponents =\n | Record<string, FC>\n | ((defaults: Record<string, FC>) => Record<string, FC>);\n\n/**\n * Resolve the effective MDX component map from a {@link BootProps.mdxComponents}\n * value (MARKDOWN_SYNTAX_SPEC §11.3):\n * - `undefined` → the platform {@link DEFAULT_MDX_COMPONENTS} (same reference).\n * - a **function** → the full-replace escape hatch, handed the defaults.\n * - a **map** → merged *over* the defaults (`{ ...defaults, ...map }`), so\n * overriding one component keeps the rest — the phantom-defaults invariant\n * (§11.2) that stops the MDX missing-reference guard from firing.\n */\nexport const resolveMdxComponents = (mdxComponents?: MdxComponents): Record<string, FC> =>\n mdxComponents === undefined\n ? (DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : typeof mdxComponents === 'function'\n ? mdxComponents(DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : { ...(DEFAULT_MDX_COMPONENTS as Record<string, FC>), ...mdxComponents };\n\n/** Options for {@link boot}: MDX overrides, a route table, or an app root. */\nexport type BootProps = {\n /**\n * MDX component overrides. A **map** is *merged over* the platform defaults\n * ({@link DEFAULT_MDX_COMPONENTS}) — so overriding `WikiLink` alone keeps the\n * default `a` and `Admonition` (MARKDOWN_SYNTAX_SPEC §11.3). Pass a **function**\n * `(defaults) => map` as the full-replace escape hatch when you want complete\n * control over the set.\n */\n mdxComponents?: MdxComponents;\n routingSpec?: RoutingSpec;\n /**\n * App root rendered directly inside the providers (with full navigation\n * context), instead of dispatching through a `routingSpec`. Render\n * `<Routes>`/`<Route>` here for fully dynamic routing — no catch-all rule\n * boilerplate. Takes precedence over `routingSpec` for what is rendered.\n */\n children?: ReactNode;\n};\n\nconst updateAlreadyApplied = (filesMetadata: FilesMetadata, update: FilesMetadata) => {\n for (let [key, value] of Object.entries(update)) {\n if (filesMetadata[key] !== value) {\n return false;\n }\n }\n return true;\n};\n\n/** The app shell {@link boot} renders: holds navigation state, subscribes to host\n * URL + metadata pushes, and renders `children` or the route `<Router />`. */\nexport const TinkerableApp = ({\n routingSpec,\n children,\n}: {\n routingSpec: RoutingSpec;\n children?: ReactNode;\n}) => {\n const [context, setContext] = useState<TinkerableState>(getInitialContext(routingSpec));\n useEffect(() => {\n const removeListener = addListener('urlchange', ({ url }) => {\n setContext((context) => {\n const updatedContext = updateContext(context, url);\n if (updatedContext !== context) {\n console.log(\n `[Sandbox] Updating path from ${context.navigationState.sandboxPath} to ${updatedContext.navigationState.sandboxPath}`\n );\n }\n return updatedContext;\n });\n });\n return removeListener;\n }, [setContext]);\n useEffect(() => {\n // Phase 5 dual-mode (SDK_PACKAGING_SPEC §4/§8): prefer the injected bundler's\n // metadata emitter (the live path, byte-identical); when the SDK is npm-fetched\n // with no injection, `event` is undefined so `addListener` receives\n // 'metadata-update' over the §4 transport instead, and `enable` is a no-op.\n const source = resolveMetadataSource(getInjectedMetadataEmitter());\n const dispose = addListener(\n 'metadata-update',\n ({ update }: Record<string, any>) => {\n setContext((prevContext) =>\n updateAlreadyApplied(prevContext.filesMetadata, update)\n ? prevContext\n : {\n ...prevContext,\n filesMetadata: {\n // TODO: file deletion!\n ...prevContext.filesMetadata,\n ...update,\n },\n }\n );\n },\n source.event\n );\n source.enable();\n return dispose;\n }, [setContext]);\n\n return (\n <TinkerableContext value={context}>\n {/* Capability C: lands cross-page `#fragment` deep-links on their section\n after the destination tree mounts, uniform for every MDX app (§13.5). */}\n <ScrollAfterNavigation />\n {children ?? <Router />}\n </TinkerableContext>\n );\n};\n\n// Boot marker emitter (LOAD_PROFILING_SPEC §3, R3-46). Rendered at the top of the\n// app tree so its layout effect fires on the FIRST root-render commit: that instant\n// is `ir.fmp` (the content is in the DOM, about to paint) and the baseline for\n// `ir.interactive` (the host treats a forwarded `ir.interactive` as the root-commit\n// signal and resolves `max(commit, reportReady)` — LP2-3 — so this can only ever be\n// delayed by an app's `reportReady()`, never advanced). Emitted in canonical stream\n// order (fmp then interactive); idempotent per name (StrictMode-safe). Renders null.\nconst BootMarkers = (): null => {\n useLayoutEffect(() => {\n emitMarkerOnce('ir.fmp');\n emitMarkerOnce('ir.interactive');\n }, []);\n return null;\n};\n\n/** The default route table when `boot` is called with no `routingSpec`/`children`:\n * `/` → main content, `/files/<path>` → the file router, else not-found.\n * Re-expressed with path templates (SANDBOX_ROUTING_SPEC §7); the file path\n * surfaces under the `*` wildcard. The catch-all stays a raw RegExp — the\n * escape hatch — so it anchors `.+` (a non-empty path) exactly as before. */\nexport const DEFAULT_ROUTING_SPEC: RoutingSpec = {\n routes: [\n { name: 'MainContent', pattern: '/', element: <MainContent /> },\n { name: 'FileRouter', pattern: `${FILES_PREFIX}/*`, element: <FileRouter /> },\n { name: 'ErrorNotFound', pattern: /^(?<path>.+)$/, element: <ErrorNotFound /> },\n ],\n};\n\n/**\n * Matches any `sandboxPath` so navigation context can be built without a route\n * table. Used when {@link boot} is given `children` (the app owns dispatch via\n * `<Routes>`); the catch-all's `element` is never rendered (children are).\n */\nexport const CATCH_ALL_ROUTING_SPEC: RoutingSpec = {\n routes: [{ name: 'AppRoot', pattern: /^.*$/, element: null }],\n};\n\n/**\n * Mount an immediately.run app into the sandbox `#root`. The entry point every\n * app calls from `index.tsx`: wires the MDX, module-cache, and navigation\n * providers, then renders the route table (`routingSpec`) or your `children`.\n */\nexport const boot = ({\n mdxComponents,\n routingSpec,\n children,\n}: BootProps = {}) => {\n const rootElement = document.getElementById('root');\n if (!rootElement) {\n throw new Error('boot requires root HTML element to exist');\n }\n const resolvedComponents = resolveMdxComponents(mdxComponents);\n // `children` own dispatch, so a catch-all keeps navigation working without a\n // table; otherwise fall back to the default file/main-content routes.\n const spec = routingSpec ?? (children ? CATCH_ALL_ROUTING_SPEC : DEFAULT_ROUTING_SPEC);\n const moduleCache = new ModuleCache();\n const root = createRoot(rootElement);\n root.render(\n <StrictMode>\n <ModuleCacheContextProvider moduleCache={moduleCache}>\n <MDXProvider components={resolvedComponents}>\n <BootMarkers />\n <TinkerableApp routingSpec={spec}>{children}</TinkerableApp>\n </MDXProvider>\n </ModuleCacheContextProvider>\n </StrictMode>\n );\n};\n"],"mappings":"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","names":["context"]}
package/dist/boot.js CHANGED
@@ -6,6 +6,7 @@ import { ErrorNotFound } from "./components/errors";
6
6
  import { FileRouter } from "./components/FileRouter";
7
7
  import { MainContent } from "./components/MainContent";
8
8
  import { DEFAULT_MDX_COMPONENTS } from "./components/MDXComponents";
9
+ import { ScrollAfterNavigation } from "./components/ScrollAfterNavigation";
9
10
  import { getInitialContext, updateContext } from "./contextUtils";
10
11
  import { getInjectedMetadataEmitter, resolveMetadataSource } from "./injectedBundler";
11
12
  import { MDXProvider } from "./MDXProvider";
@@ -63,7 +64,10 @@ const TinkerableApp = ({
63
64
  source.enable();
64
65
  return dispose;
65
66
  }, [setContext]);
66
- return /* @__PURE__ */ jsx(TinkerableContext, { value: context, children: children ?? /* @__PURE__ */ jsx(Router, {}) });
67
+ return /* @__PURE__ */ jsxs(TinkerableContext, { value: context, children: [
68
+ /* @__PURE__ */ jsx(ScrollAfterNavigation, {}),
69
+ children ?? /* @__PURE__ */ jsx(Router, {})
70
+ ] });
67
71
  };
68
72
  const BootMarkers = () => {
69
73
  useLayoutEffect(() => {
package/dist/boot.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/boot.tsx"],"sourcesContent":["import { FC, ReactNode, StrictMode, useEffect, useLayoutEffect, useState } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport { emitMarkerOnce } from './markers';\n\nimport { ErrorNotFound } from './components/errors';\nimport { FileRouter } from './components/FileRouter';\nimport { MainContent } from './components/MainContent';\nimport { DEFAULT_MDX_COMPONENTS } from './components/MDXComponents';\nimport { getInitialContext, updateContext } from './contextUtils';\nimport { getInjectedMetadataEmitter, resolveMetadataSource } from './injectedBundler';\nimport { MDXProvider } from './MDXProvider';\nimport { ModuleCache, ModuleCacheContextProvider } from './moduleCache';\nimport { Router } from './routing';\nimport type { RoutingSpec } from './RoutingSpec';\nimport { FilesMetadata } from './sandboxTypes';\nimport { addListener } from './sandboxUtils';\nimport { TinkerableContext, TinkerableState } from './TinkerableContext';\nimport { FILES_PREFIX } from './urlUtils';\n\n/** A map of MDX component overrides, or a function that receives the platform\n * {@link DEFAULT_MDX_COMPONENTS} and returns the full map to use. */\nexport type MdxComponents =\n | Record<string, FC>\n | ((defaults: Record<string, FC>) => Record<string, FC>);\n\n/**\n * Resolve the effective MDX component map from a {@link BootProps.mdxComponents}\n * value (MARKDOWN_SYNTAX_SPEC §11.3):\n * - `undefined` → the platform {@link DEFAULT_MDX_COMPONENTS} (same reference).\n * - a **function** → the full-replace escape hatch, handed the defaults.\n * - a **map** → merged *over* the defaults (`{ ...defaults, ...map }`), so\n * overriding one component keeps the rest — the phantom-defaults invariant\n * (§11.2) that stops the MDX missing-reference guard from firing.\n */\nexport const resolveMdxComponents = (mdxComponents?: MdxComponents): Record<string, FC> =>\n mdxComponents === undefined\n ? (DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : typeof mdxComponents === 'function'\n ? mdxComponents(DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : { ...(DEFAULT_MDX_COMPONENTS as Record<string, FC>), ...mdxComponents };\n\n/** Options for {@link boot}: MDX overrides, a route table, or an app root. */\nexport type BootProps = {\n /**\n * MDX component overrides. A **map** is *merged over* the platform defaults\n * ({@link DEFAULT_MDX_COMPONENTS}) — so overriding `WikiLink` alone keeps the\n * default `a` and `Admonition` (MARKDOWN_SYNTAX_SPEC §11.3). Pass a **function**\n * `(defaults) => map` as the full-replace escape hatch when you want complete\n * control over the set.\n */\n mdxComponents?: MdxComponents;\n routingSpec?: RoutingSpec;\n /**\n * App root rendered directly inside the providers (with full navigation\n * context), instead of dispatching through a `routingSpec`. Render\n * `<Routes>`/`<Route>` here for fully dynamic routing — no catch-all rule\n * boilerplate. Takes precedence over `routingSpec` for what is rendered.\n */\n children?: ReactNode;\n};\n\nconst updateAlreadyApplied = (filesMetadata: FilesMetadata, update: FilesMetadata) => {\n for (let [key, value] of Object.entries(update)) {\n if (filesMetadata[key] !== value) {\n return false;\n }\n }\n return true;\n};\n\n/** The app shell {@link boot} renders: holds navigation state, subscribes to host\n * URL + metadata pushes, and renders `children` or the route `<Router />`. */\nexport const TinkerableApp = ({\n routingSpec,\n children,\n}: {\n routingSpec: RoutingSpec;\n children?: ReactNode;\n}) => {\n const [context, setContext] = useState<TinkerableState>(getInitialContext(routingSpec));\n useEffect(() => {\n const removeListener = addListener('urlchange', ({ url }) => {\n setContext((context) => {\n const updatedContext = updateContext(context, url);\n if (updatedContext !== context) {\n console.log(\n `[Sandbox] Updating path from ${context.navigationState.sandboxPath} to ${updatedContext.navigationState.sandboxPath}`\n );\n }\n return updatedContext;\n });\n });\n return removeListener;\n }, [setContext]);\n useEffect(() => {\n // Phase 5 dual-mode (SDK_PACKAGING_SPEC §4/§8): prefer the injected bundler's\n // metadata emitter (the live path, byte-identical); when the SDK is npm-fetched\n // with no injection, `event` is undefined so `addListener` receives\n // 'metadata-update' over the §4 transport instead, and `enable` is a no-op.\n const source = resolveMetadataSource(getInjectedMetadataEmitter());\n const dispose = addListener(\n 'metadata-update',\n ({ update }: Record<string, any>) => {\n setContext((prevContext) =>\n updateAlreadyApplied(prevContext.filesMetadata, update)\n ? prevContext\n : {\n ...prevContext,\n filesMetadata: {\n // TODO: file deletion!\n ...prevContext.filesMetadata,\n ...update,\n },\n }\n );\n },\n source.event\n );\n source.enable();\n return dispose;\n }, [setContext]);\n\n return (\n <TinkerableContext value={context}>\n {children ?? <Router />}\n </TinkerableContext>\n );\n};\n\n// Boot marker emitter (LOAD_PROFILING_SPEC §3, R3-46). Rendered at the top of the\n// app tree so its layout effect fires on the FIRST root-render commit: that instant\n// is `ir.fmp` (the content is in the DOM, about to paint) and the baseline for\n// `ir.interactive` (the host treats a forwarded `ir.interactive` as the root-commit\n// signal and resolves `max(commit, reportReady)` — LP2-3 — so this can only ever be\n// delayed by an app's `reportReady()`, never advanced). Emitted in canonical stream\n// order (fmp then interactive); idempotent per name (StrictMode-safe). Renders null.\nconst BootMarkers = (): null => {\n useLayoutEffect(() => {\n emitMarkerOnce('ir.fmp');\n emitMarkerOnce('ir.interactive');\n }, []);\n return null;\n};\n\n/** The default route table when `boot` is called with no `routingSpec`/`children`:\n * `/` → main content, `/files/<path>` → the file router, else not-found.\n * Re-expressed with path templates (SANDBOX_ROUTING_SPEC §7); the file path\n * surfaces under the `*` wildcard. The catch-all stays a raw RegExp — the\n * escape hatch — so it anchors `.+` (a non-empty path) exactly as before. */\nexport const DEFAULT_ROUTING_SPEC: RoutingSpec = {\n routes: [\n { name: 'MainContent', pattern: '/', element: <MainContent /> },\n { name: 'FileRouter', pattern: `${FILES_PREFIX}/*`, element: <FileRouter /> },\n { name: 'ErrorNotFound', pattern: /^(?<path>.+)$/, element: <ErrorNotFound /> },\n ],\n};\n\n/**\n * Matches any `sandboxPath` so navigation context can be built without a route\n * table. Used when {@link boot} is given `children` (the app owns dispatch via\n * `<Routes>`); the catch-all's `element` is never rendered (children are).\n */\nexport const CATCH_ALL_ROUTING_SPEC: RoutingSpec = {\n routes: [{ name: 'AppRoot', pattern: /^.*$/, element: null }],\n};\n\n/**\n * Mount an immediately.run app into the sandbox `#root`. The entry point every\n * app calls from `index.tsx`: wires the MDX, module-cache, and navigation\n * providers, then renders the route table (`routingSpec`) or your `children`.\n */\nexport const boot = ({\n mdxComponents,\n routingSpec,\n children,\n}: BootProps = {}) => {\n const rootElement = document.getElementById('root');\n if (!rootElement) {\n throw new Error('boot requires root HTML element to exist');\n }\n const resolvedComponents = resolveMdxComponents(mdxComponents);\n // `children` own dispatch, so a catch-all keeps navigation working without a\n // table; otherwise fall back to the default file/main-content routes.\n const spec = routingSpec ?? (children ? CATCH_ALL_ROUTING_SPEC : DEFAULT_ROUTING_SPEC);\n const moduleCache = new ModuleCache();\n const root = createRoot(rootElement);\n root.render(\n <StrictMode>\n <ModuleCacheContextProvider moduleCache={moduleCache}>\n <MDXProvider components={resolvedComponents}>\n <BootMarkers />\n <TinkerableApp routingSpec={spec}>{children}</TinkerableApp>\n </MDXProvider>\n </ModuleCacheContextProvider>\n </StrictMode>\n );\n};\n"],"mappings":"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","names":["context"]}
1
+ {"version":3,"sources":["../src/boot.tsx"],"sourcesContent":["import { FC, ReactNode, StrictMode, useEffect, useLayoutEffect, useState } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport { emitMarkerOnce } from './markers';\n\nimport { ErrorNotFound } from './components/errors';\nimport { FileRouter } from './components/FileRouter';\nimport { MainContent } from './components/MainContent';\nimport { DEFAULT_MDX_COMPONENTS } from './components/MDXComponents';\nimport { ScrollAfterNavigation } from './components/ScrollAfterNavigation';\nimport { getInitialContext, updateContext } from './contextUtils';\nimport { getInjectedMetadataEmitter, resolveMetadataSource } from './injectedBundler';\nimport { MDXProvider } from './MDXProvider';\nimport { ModuleCache, ModuleCacheContextProvider } from './moduleCache';\nimport { Router } from './routing';\nimport type { RoutingSpec } from './RoutingSpec';\nimport { FilesMetadata } from './sandboxTypes';\nimport { addListener } from './sandboxUtils';\nimport { TinkerableContext, TinkerableState } from './TinkerableContext';\nimport { FILES_PREFIX } from './urlUtils';\n\n/** A map of MDX component overrides, or a function that receives the platform\n * {@link DEFAULT_MDX_COMPONENTS} and returns the full map to use. */\nexport type MdxComponents =\n | Record<string, FC>\n | ((defaults: Record<string, FC>) => Record<string, FC>);\n\n/**\n * Resolve the effective MDX component map from a {@link BootProps.mdxComponents}\n * value (MARKDOWN_SYNTAX_SPEC §11.3):\n * - `undefined` → the platform {@link DEFAULT_MDX_COMPONENTS} (same reference).\n * - a **function** → the full-replace escape hatch, handed the defaults.\n * - a **map** → merged *over* the defaults (`{ ...defaults, ...map }`), so\n * overriding one component keeps the rest — the phantom-defaults invariant\n * (§11.2) that stops the MDX missing-reference guard from firing.\n */\nexport const resolveMdxComponents = (mdxComponents?: MdxComponents): Record<string, FC> =>\n mdxComponents === undefined\n ? (DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : typeof mdxComponents === 'function'\n ? mdxComponents(DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : { ...(DEFAULT_MDX_COMPONENTS as Record<string, FC>), ...mdxComponents };\n\n/** Options for {@link boot}: MDX overrides, a route table, or an app root. */\nexport type BootProps = {\n /**\n * MDX component overrides. A **map** is *merged over* the platform defaults\n * ({@link DEFAULT_MDX_COMPONENTS}) — so overriding `WikiLink` alone keeps the\n * default `a` and `Admonition` (MARKDOWN_SYNTAX_SPEC §11.3). Pass a **function**\n * `(defaults) => map` as the full-replace escape hatch when you want complete\n * control over the set.\n */\n mdxComponents?: MdxComponents;\n routingSpec?: RoutingSpec;\n /**\n * App root rendered directly inside the providers (with full navigation\n * context), instead of dispatching through a `routingSpec`. Render\n * `<Routes>`/`<Route>` here for fully dynamic routing — no catch-all rule\n * boilerplate. Takes precedence over `routingSpec` for what is rendered.\n */\n children?: ReactNode;\n};\n\nconst updateAlreadyApplied = (filesMetadata: FilesMetadata, update: FilesMetadata) => {\n for (let [key, value] of Object.entries(update)) {\n if (filesMetadata[key] !== value) {\n return false;\n }\n }\n return true;\n};\n\n/** The app shell {@link boot} renders: holds navigation state, subscribes to host\n * URL + metadata pushes, and renders `children` or the route `<Router />`. */\nexport const TinkerableApp = ({\n routingSpec,\n children,\n}: {\n routingSpec: RoutingSpec;\n children?: ReactNode;\n}) => {\n const [context, setContext] = useState<TinkerableState>(getInitialContext(routingSpec));\n useEffect(() => {\n const removeListener = addListener('urlchange', ({ url }) => {\n setContext((context) => {\n const updatedContext = updateContext(context, url);\n if (updatedContext !== context) {\n console.log(\n `[Sandbox] Updating path from ${context.navigationState.sandboxPath} to ${updatedContext.navigationState.sandboxPath}`\n );\n }\n return updatedContext;\n });\n });\n return removeListener;\n }, [setContext]);\n useEffect(() => {\n // Phase 5 dual-mode (SDK_PACKAGING_SPEC §4/§8): prefer the injected bundler's\n // metadata emitter (the live path, byte-identical); when the SDK is npm-fetched\n // with no injection, `event` is undefined so `addListener` receives\n // 'metadata-update' over the §4 transport instead, and `enable` is a no-op.\n const source = resolveMetadataSource(getInjectedMetadataEmitter());\n const dispose = addListener(\n 'metadata-update',\n ({ update }: Record<string, any>) => {\n setContext((prevContext) =>\n updateAlreadyApplied(prevContext.filesMetadata, update)\n ? prevContext\n : {\n ...prevContext,\n filesMetadata: {\n // TODO: file deletion!\n ...prevContext.filesMetadata,\n ...update,\n },\n }\n );\n },\n source.event\n );\n source.enable();\n return dispose;\n }, [setContext]);\n\n return (\n <TinkerableContext value={context}>\n {/* Capability C: lands cross-page `#fragment` deep-links on their section\n after the destination tree mounts, uniform for every MDX app (§13.5). */}\n <ScrollAfterNavigation />\n {children ?? <Router />}\n </TinkerableContext>\n );\n};\n\n// Boot marker emitter (LOAD_PROFILING_SPEC §3, R3-46). Rendered at the top of the\n// app tree so its layout effect fires on the FIRST root-render commit: that instant\n// is `ir.fmp` (the content is in the DOM, about to paint) and the baseline for\n// `ir.interactive` (the host treats a forwarded `ir.interactive` as the root-commit\n// signal and resolves `max(commit, reportReady)` — LP2-3 — so this can only ever be\n// delayed by an app's `reportReady()`, never advanced). Emitted in canonical stream\n// order (fmp then interactive); idempotent per name (StrictMode-safe). Renders null.\nconst BootMarkers = (): null => {\n useLayoutEffect(() => {\n emitMarkerOnce('ir.fmp');\n emitMarkerOnce('ir.interactive');\n }, []);\n return null;\n};\n\n/** The default route table when `boot` is called with no `routingSpec`/`children`:\n * `/` → main content, `/files/<path>` → the file router, else not-found.\n * Re-expressed with path templates (SANDBOX_ROUTING_SPEC §7); the file path\n * surfaces under the `*` wildcard. The catch-all stays a raw RegExp — the\n * escape hatch — so it anchors `.+` (a non-empty path) exactly as before. */\nexport const DEFAULT_ROUTING_SPEC: RoutingSpec = {\n routes: [\n { name: 'MainContent', pattern: '/', element: <MainContent /> },\n { name: 'FileRouter', pattern: `${FILES_PREFIX}/*`, element: <FileRouter /> },\n { name: 'ErrorNotFound', pattern: /^(?<path>.+)$/, element: <ErrorNotFound /> },\n ],\n};\n\n/**\n * Matches any `sandboxPath` so navigation context can be built without a route\n * table. Used when {@link boot} is given `children` (the app owns dispatch via\n * `<Routes>`); the catch-all's `element` is never rendered (children are).\n */\nexport const CATCH_ALL_ROUTING_SPEC: RoutingSpec = {\n routes: [{ name: 'AppRoot', pattern: /^.*$/, element: null }],\n};\n\n/**\n * Mount an immediately.run app into the sandbox `#root`. The entry point every\n * app calls from `index.tsx`: wires the MDX, module-cache, and navigation\n * providers, then renders the route table (`routingSpec`) or your `children`.\n */\nexport const boot = ({\n mdxComponents,\n routingSpec,\n children,\n}: BootProps = {}) => {\n const rootElement = document.getElementById('root');\n if (!rootElement) {\n throw new Error('boot requires root HTML element to exist');\n }\n const resolvedComponents = resolveMdxComponents(mdxComponents);\n // `children` own dispatch, so a catch-all keeps navigation working without a\n // table; otherwise fall back to the default file/main-content routes.\n const spec = routingSpec ?? (children ? CATCH_ALL_ROUTING_SPEC : DEFAULT_ROUTING_SPEC);\n const moduleCache = new ModuleCache();\n const root = createRoot(rootElement);\n root.render(\n <StrictMode>\n <ModuleCacheContextProvider moduleCache={moduleCache}>\n <MDXProvider components={resolvedComponents}>\n <BootMarkers />\n <TinkerableApp routingSpec={spec}>{children}</TinkerableApp>\n </MDXProvider>\n </ModuleCacheContextProvider>\n </StrictMode>\n );\n};\n"],"mappings":"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","names":["context"]}
@@ -18,6 +18,7 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var Link_exports = {};
20
20
  __export(Link_exports, {
21
+ FragmentLink: () => FragmentLink,
21
22
  InternalLink: () => InternalLink,
22
23
  Link: () => Link
23
24
  });
@@ -25,8 +26,28 @@ module.exports = __toCommonJS(Link_exports);
25
26
  var import_jsx_runtime = require("react/jsx-runtime");
26
27
  var import_react = require("react");
27
28
  var import_routing = require("../routing");
29
+ var import_scrollToId = require("../scrollToId");
28
30
  var import_TinkerableContext = require("../TinkerableContext");
29
31
  var import_urlUtils = require("../urlUtils");
32
+ const FragmentLink = ({
33
+ href,
34
+ children,
35
+ onClick,
36
+ ...props
37
+ }) => {
38
+ const clickHandler = (0, import_react.useCallback)(
39
+ (e) => {
40
+ onClick?.(e);
41
+ if (e.defaultPrevented) return;
42
+ if (href && href.startsWith("#")) {
43
+ e.preventDefault();
44
+ (0, import_scrollToId.scrollToId)(href.slice(1));
45
+ }
46
+ },
47
+ [href, onClick]
48
+ );
49
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href, onClick: clickHandler, ...props, children });
50
+ };
30
51
  const InternalLink = ({
31
52
  href,
32
53
  children,
@@ -49,6 +70,9 @@ const Link = ({
49
70
  ...properties
50
71
  }) => {
51
72
  const { outerHref, navigationState } = (0, import_react.use)(import_TinkerableContext.TinkerableContext);
73
+ if (href && href.startsWith("#")) {
74
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FragmentLink, { href, ...properties, children });
75
+ }
52
76
  if (href && (0, import_urlUtils.isInternalHref)(outerHref, href, navigationState)) {
53
77
  const targetHref = (0, import_urlUtils.constructOuterUrl)(outerHref, href, navigationState);
54
78
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InternalLink, { href: targetHref, ...properties, children });
@@ -58,6 +82,7 @@ const Link = ({
58
82
  };
59
83
  // Annotate the CommonJS export names for ESM import in node:
60
84
  0 && (module.exports = {
85
+ FragmentLink,
61
86
  InternalLink,
62
87
  Link
63
88
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/Link.tsx"],"sourcesContent":["import { ReactNode, use, useCallback } from 'react';\nimport { navigate } from '../routing';\nimport { TinkerableContext } from '../TinkerableContext';\nimport { constructOuterUrl, isInternalHref } from '../urlUtils';\n\n/** An `<a>` that performs in-sandbox navigation on click (prevents the default\n * full-page load and routes via {@link navigate}). */\nexport const InternalLink = ({\n href,\n children,\n ...props\n}: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>): ReactNode => {\n const clickHandler = useCallback(\n (e: any) => {\n if (href) {\n e.preventDefault();\n navigate(href);\n }\n },\n [href]\n );\n return (\n <a href={href} onClick={clickHandler} {...props}>\n {children}\n </a>\n );\n};\n\n/** A link that routes same-app hrefs through the sandbox router (as an\n * {@link InternalLink}) and renders external hrefs as a plain `<a>`. */\nexport const Link = ({\n href,\n children,\n ...properties\n}: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>): ReactNode => {\n const { outerHref, navigationState } = use(TinkerableContext);\n if (href && isInternalHref(outerHref, href, navigationState)) {\n const targetHref = constructOuterUrl(outerHref, href, navigationState);\n return (\n <InternalLink href={targetHref} {...properties}>\n {children}\n </InternalLink>\n );\n } else {\n // create a regular link to external resource\n return <a {...{ href, ...properties }}>{children}</a>;\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBI;AAtBJ,mBAA4C;AAC5C,qBAAyB;AACzB,+BAAkC;AAClC,sBAAkD;AAI3C,MAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA4G;AAC1G,QAAM,mBAAe;AAAA,IACnB,CAAC,MAAW;AACV,UAAI,MAAM;AACR,UAAE,eAAe;AACjB,qCAAS,IAAI;AAAA,MACf;AAAA,IACF;AAAA,IACA,CAAC,IAAI;AAAA,EACP;AACA,SACE,4CAAC,OAAE,MAAY,SAAS,cAAe,GAAG,OACvC,UACH;AAEJ;AAIO,MAAM,OAAO,CAAC;AAAA,EACnB;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA4G;AAC1G,QAAM,EAAE,WAAW,gBAAgB,QAAI,kBAAI,0CAAiB;AAC5D,MAAI,YAAQ,gCAAe,WAAW,MAAM,eAAe,GAAG;AAC5D,UAAM,iBAAa,mCAAkB,WAAW,MAAM,eAAe;AACrE,WACE,4CAAC,gBAAa,MAAM,YAAa,GAAG,YACjC,UACH;AAAA,EAEJ,OAAO;AAEL,WAAO,4CAAC,OAAG,GAAG,EAAE,MAAM,GAAG,WAAW,GAAI,UAAS;AAAA,EACnD;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/components/Link.tsx"],"sourcesContent":["import { ReactNode, use, useCallback } from 'react';\nimport { navigate } from '../routing';\nimport { scrollToId } from '../scrollToId';\nimport { TinkerableContext } from '../TinkerableContext';\nimport { constructOuterUrl, isInternalHref } from '../urlUtils';\n\n/** A same-page anchor (`#frag`): scrolls the addressed section into view on click\n * **without a route change** (MARKDOWN_SYNTAX_SPEC §13.5). The default behavior of a\n * bare `#`-href is intercepted so the sandbox URL the host owns is never mutated\n * out from under it; a fragment that names nothing leaves the scroll position\n * untouched (a soft failure). */\nexport const FragmentLink = ({\n href,\n children,\n onClick,\n ...props\n}: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>): ReactNode => {\n const clickHandler = useCallback(\n (e: React.MouseEvent<HTMLAnchorElement>) => {\n onClick?.(e);\n if (e.defaultPrevented) return;\n if (href && href.startsWith('#')) {\n e.preventDefault();\n scrollToId(href.slice(1));\n }\n },\n [href, onClick]\n );\n return (\n <a href={href} onClick={clickHandler} {...props}>\n {children}\n </a>\n );\n};\n\n/** An `<a>` that performs in-sandbox navigation on click (prevents the default\n * full-page load and routes via {@link navigate}). */\nexport const InternalLink = ({\n href,\n children,\n ...props\n}: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>): ReactNode => {\n const clickHandler = useCallback(\n (e: any) => {\n if (href) {\n e.preventDefault();\n navigate(href);\n }\n },\n [href]\n );\n return (\n <a href={href} onClick={clickHandler} {...props}>\n {children}\n </a>\n );\n};\n\n/** A link that routes same-app hrefs through the sandbox router (as an\n * {@link InternalLink}) and renders external hrefs as a plain `<a>`. */\nexport const Link = ({\n href,\n children,\n ...properties\n}: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>): ReactNode => {\n const { outerHref, navigationState } = use(TinkerableContext);\n // A pure same-page fragment (`#sec-8-9`) scrolls in place — no route change (§13.5).\n if (href && href.startsWith('#')) {\n return (\n <FragmentLink href={href} {...properties}>\n {children}\n </FragmentLink>\n );\n }\n if (href && isInternalHref(outerHref, href, navigationState)) {\n const targetHref = constructOuterUrl(outerHref, href, navigationState);\n return (\n <InternalLink href={targetHref} {...properties}>\n {children}\n </InternalLink>\n );\n } else {\n // create a regular link to external resource\n return <a {...{ href, ...properties }}>{children}</a>;\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6BI;AA7BJ,mBAA4C;AAC5C,qBAAyB;AACzB,wBAA2B;AAC3B,+BAAkC;AAClC,sBAAkD;AAO3C,MAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA4G;AAC1G,QAAM,mBAAe;AAAA,IACnB,CAAC,MAA2C;AAC1C,gBAAU,CAAC;AACX,UAAI,EAAE,iBAAkB;AACxB,UAAI,QAAQ,KAAK,WAAW,GAAG,GAAG;AAChC,UAAE,eAAe;AACjB,0CAAW,KAAK,MAAM,CAAC,CAAC;AAAA,MAC1B;AAAA,IACF;AAAA,IACA,CAAC,MAAM,OAAO;AAAA,EAChB;AACA,SACE,4CAAC,OAAE,MAAY,SAAS,cAAe,GAAG,OACvC,UACH;AAEJ;AAIO,MAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA4G;AAC1G,QAAM,mBAAe;AAAA,IACnB,CAAC,MAAW;AACV,UAAI,MAAM;AACR,UAAE,eAAe;AACjB,qCAAS,IAAI;AAAA,MACf;AAAA,IACF;AAAA,IACA,CAAC,IAAI;AAAA,EACP;AACA,SACE,4CAAC,OAAE,MAAY,SAAS,cAAe,GAAG,OACvC,UACH;AAEJ;AAIO,MAAM,OAAO,CAAC;AAAA,EACnB;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA4G;AAC1G,QAAM,EAAE,WAAW,gBAAgB,QAAI,kBAAI,0CAAiB;AAE5D,MAAI,QAAQ,KAAK,WAAW,GAAG,GAAG;AAChC,WACE,4CAAC,gBAAa,MAAa,GAAG,YAC3B,UACH;AAAA,EAEJ;AACA,MAAI,YAAQ,gCAAe,WAAW,MAAM,eAAe,GAAG;AAC5D,UAAM,iBAAa,mCAAkB,WAAW,MAAM,eAAe;AACrE,WACE,4CAAC,gBAAa,MAAM,YAAa,GAAG,YACjC,UACH;AAAA,EAEJ,OAAO;AAEL,WAAO,4CAAC,OAAG,GAAG,EAAE,MAAM,GAAG,WAAW,GAAI,UAAS;AAAA,EACnD;AACF;","names":[]}
@@ -1,5 +1,11 @@
1
1
  import { ReactNode } from 'react';
2
2
 
3
+ /** A same-page anchor (`#frag`): scrolls the addressed section into view on click
4
+ * **without a route change** (MARKDOWN_SYNTAX_SPEC §13.5). The default behavior of a
5
+ * bare `#`-href is intercepted so the sandbox URL the host owns is never mutated
6
+ * out from under it; a fragment that names nothing leaves the scroll position
7
+ * untouched (a soft failure). */
8
+ declare const FragmentLink: ({ href, children, onClick, ...props }: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => ReactNode;
3
9
  /** An `<a>` that performs in-sandbox navigation on click (prevents the default
4
10
  * full-page load and routes via {@link navigate}). */
5
11
  declare const InternalLink: ({ href, children, ...props }: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => ReactNode;
@@ -7,4 +13,4 @@ declare const InternalLink: ({ href, children, ...props }: React.DetailedHTMLPro
7
13
  * {@link InternalLink}) and renders external hrefs as a plain `<a>`. */
8
14
  declare const Link: ({ href, children, ...properties }: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => ReactNode;
9
15
 
10
- export { InternalLink, Link };
16
+ export { FragmentLink, InternalLink, Link };
@@ -1,5 +1,11 @@
1
1
  import { ReactNode } from 'react';
2
2
 
3
+ /** A same-page anchor (`#frag`): scrolls the addressed section into view on click
4
+ * **without a route change** (MARKDOWN_SYNTAX_SPEC §13.5). The default behavior of a
5
+ * bare `#`-href is intercepted so the sandbox URL the host owns is never mutated
6
+ * out from under it; a fragment that names nothing leaves the scroll position
7
+ * untouched (a soft failure). */
8
+ declare const FragmentLink: ({ href, children, onClick, ...props }: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => ReactNode;
3
9
  /** An `<a>` that performs in-sandbox navigation on click (prevents the default
4
10
  * full-page load and routes via {@link navigate}). */
5
11
  declare const InternalLink: ({ href, children, ...props }: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => ReactNode;
@@ -7,4 +13,4 @@ declare const InternalLink: ({ href, children, ...props }: React.DetailedHTMLPro
7
13
  * {@link InternalLink}) and renders external hrefs as a plain `<a>`. */
8
14
  declare const Link: ({ href, children, ...properties }: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => ReactNode;
9
15
 
10
- export { InternalLink, Link };
16
+ export { FragmentLink, InternalLink, Link };
@@ -1,8 +1,28 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { use, useCallback } from "react";
3
3
  import { navigate } from "../routing";
4
+ import { scrollToId } from "../scrollToId";
4
5
  import { TinkerableContext } from "../TinkerableContext";
5
6
  import { constructOuterUrl, isInternalHref } from "../urlUtils";
7
+ const FragmentLink = ({
8
+ href,
9
+ children,
10
+ onClick,
11
+ ...props
12
+ }) => {
13
+ const clickHandler = useCallback(
14
+ (e) => {
15
+ onClick?.(e);
16
+ if (e.defaultPrevented) return;
17
+ if (href && href.startsWith("#")) {
18
+ e.preventDefault();
19
+ scrollToId(href.slice(1));
20
+ }
21
+ },
22
+ [href, onClick]
23
+ );
24
+ return /* @__PURE__ */ jsx("a", { href, onClick: clickHandler, ...props, children });
25
+ };
6
26
  const InternalLink = ({
7
27
  href,
8
28
  children,
@@ -25,6 +45,9 @@ const Link = ({
25
45
  ...properties
26
46
  }) => {
27
47
  const { outerHref, navigationState } = use(TinkerableContext);
48
+ if (href && href.startsWith("#")) {
49
+ return /* @__PURE__ */ jsx(FragmentLink, { href, ...properties, children });
50
+ }
28
51
  if (href && isInternalHref(outerHref, href, navigationState)) {
29
52
  const targetHref = constructOuterUrl(outerHref, href, navigationState);
30
53
  return /* @__PURE__ */ jsx(InternalLink, { href: targetHref, ...properties, children });
@@ -33,6 +56,7 @@ const Link = ({
33
56
  }
34
57
  };
35
58
  export {
59
+ FragmentLink,
36
60
  InternalLink,
37
61
  Link
38
62
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/Link.tsx"],"sourcesContent":["import { ReactNode, use, useCallback } from 'react';\nimport { navigate } from '../routing';\nimport { TinkerableContext } from '../TinkerableContext';\nimport { constructOuterUrl, isInternalHref } from '../urlUtils';\n\n/** An `<a>` that performs in-sandbox navigation on click (prevents the default\n * full-page load and routes via {@link navigate}). */\nexport const InternalLink = ({\n href,\n children,\n ...props\n}: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>): ReactNode => {\n const clickHandler = useCallback(\n (e: any) => {\n if (href) {\n e.preventDefault();\n navigate(href);\n }\n },\n [href]\n );\n return (\n <a href={href} onClick={clickHandler} {...props}>\n {children}\n </a>\n );\n};\n\n/** A link that routes same-app hrefs through the sandbox router (as an\n * {@link InternalLink}) and renders external hrefs as a plain `<a>`. */\nexport const Link = ({\n href,\n children,\n ...properties\n}: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>): ReactNode => {\n const { outerHref, navigationState } = use(TinkerableContext);\n if (href && isInternalHref(outerHref, href, navigationState)) {\n const targetHref = constructOuterUrl(outerHref, href, navigationState);\n return (\n <InternalLink href={targetHref} {...properties}>\n {children}\n </InternalLink>\n );\n } else {\n // create a regular link to external resource\n return <a {...{ href, ...properties }}>{children}</a>;\n }\n};\n"],"mappings":"AAsBI;AAtBJ,SAAoB,KAAK,mBAAmB;AAC5C,SAAS,gBAAgB;AACzB,SAAS,yBAAyB;AAClC,SAAS,mBAAmB,sBAAsB;AAI3C,MAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA4G;AAC1G,QAAM,eAAe;AAAA,IACnB,CAAC,MAAW;AACV,UAAI,MAAM;AACR,UAAE,eAAe;AACjB,iBAAS,IAAI;AAAA,MACf;AAAA,IACF;AAAA,IACA,CAAC,IAAI;AAAA,EACP;AACA,SACE,oBAAC,OAAE,MAAY,SAAS,cAAe,GAAG,OACvC,UACH;AAEJ;AAIO,MAAM,OAAO,CAAC;AAAA,EACnB;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA4G;AAC1G,QAAM,EAAE,WAAW,gBAAgB,IAAI,IAAI,iBAAiB;AAC5D,MAAI,QAAQ,eAAe,WAAW,MAAM,eAAe,GAAG;AAC5D,UAAM,aAAa,kBAAkB,WAAW,MAAM,eAAe;AACrE,WACE,oBAAC,gBAAa,MAAM,YAAa,GAAG,YACjC,UACH;AAAA,EAEJ,OAAO;AAEL,WAAO,oBAAC,OAAG,GAAG,EAAE,MAAM,GAAG,WAAW,GAAI,UAAS;AAAA,EACnD;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/components/Link.tsx"],"sourcesContent":["import { ReactNode, use, useCallback } from 'react';\nimport { navigate } from '../routing';\nimport { scrollToId } from '../scrollToId';\nimport { TinkerableContext } from '../TinkerableContext';\nimport { constructOuterUrl, isInternalHref } from '../urlUtils';\n\n/** A same-page anchor (`#frag`): scrolls the addressed section into view on click\n * **without a route change** (MARKDOWN_SYNTAX_SPEC §13.5). The default behavior of a\n * bare `#`-href is intercepted so the sandbox URL the host owns is never mutated\n * out from under it; a fragment that names nothing leaves the scroll position\n * untouched (a soft failure). */\nexport const FragmentLink = ({\n href,\n children,\n onClick,\n ...props\n}: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>): ReactNode => {\n const clickHandler = useCallback(\n (e: React.MouseEvent<HTMLAnchorElement>) => {\n onClick?.(e);\n if (e.defaultPrevented) return;\n if (href && href.startsWith('#')) {\n e.preventDefault();\n scrollToId(href.slice(1));\n }\n },\n [href, onClick]\n );\n return (\n <a href={href} onClick={clickHandler} {...props}>\n {children}\n </a>\n );\n};\n\n/** An `<a>` that performs in-sandbox navigation on click (prevents the default\n * full-page load and routes via {@link navigate}). */\nexport const InternalLink = ({\n href,\n children,\n ...props\n}: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>): ReactNode => {\n const clickHandler = useCallback(\n (e: any) => {\n if (href) {\n e.preventDefault();\n navigate(href);\n }\n },\n [href]\n );\n return (\n <a href={href} onClick={clickHandler} {...props}>\n {children}\n </a>\n );\n};\n\n/** A link that routes same-app hrefs through the sandbox router (as an\n * {@link InternalLink}) and renders external hrefs as a plain `<a>`. */\nexport const Link = ({\n href,\n children,\n ...properties\n}: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>): ReactNode => {\n const { outerHref, navigationState } = use(TinkerableContext);\n // A pure same-page fragment (`#sec-8-9`) scrolls in place — no route change (§13.5).\n if (href && href.startsWith('#')) {\n return (\n <FragmentLink href={href} {...properties}>\n {children}\n </FragmentLink>\n );\n }\n if (href && isInternalHref(outerHref, href, navigationState)) {\n const targetHref = constructOuterUrl(outerHref, href, navigationState);\n return (\n <InternalLink href={targetHref} {...properties}>\n {children}\n </InternalLink>\n );\n } else {\n // create a regular link to external resource\n return <a {...{ href, ...properties }}>{children}</a>;\n }\n};\n"],"mappings":"AA6BI;AA7BJ,SAAoB,KAAK,mBAAmB;AAC5C,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC,SAAS,mBAAmB,sBAAsB;AAO3C,MAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA4G;AAC1G,QAAM,eAAe;AAAA,IACnB,CAAC,MAA2C;AAC1C,gBAAU,CAAC;AACX,UAAI,EAAE,iBAAkB;AACxB,UAAI,QAAQ,KAAK,WAAW,GAAG,GAAG;AAChC,UAAE,eAAe;AACjB,mBAAW,KAAK,MAAM,CAAC,CAAC;AAAA,MAC1B;AAAA,IACF;AAAA,IACA,CAAC,MAAM,OAAO;AAAA,EAChB;AACA,SACE,oBAAC,OAAE,MAAY,SAAS,cAAe,GAAG,OACvC,UACH;AAEJ;AAIO,MAAM,eAAe,CAAC;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA4G;AAC1G,QAAM,eAAe;AAAA,IACnB,CAAC,MAAW;AACV,UAAI,MAAM;AACR,UAAE,eAAe;AACjB,iBAAS,IAAI;AAAA,MACf;AAAA,IACF;AAAA,IACA,CAAC,IAAI;AAAA,EACP;AACA,SACE,oBAAC,OAAE,MAAY,SAAS,cAAe,GAAG,OACvC,UACH;AAEJ;AAIO,MAAM,OAAO,CAAC;AAAA,EACnB;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAA4G;AAC1G,QAAM,EAAE,WAAW,gBAAgB,IAAI,IAAI,iBAAiB;AAE5D,MAAI,QAAQ,KAAK,WAAW,GAAG,GAAG;AAChC,WACE,oBAAC,gBAAa,MAAa,GAAG,YAC3B,UACH;AAAA,EAEJ;AACA,MAAI,QAAQ,eAAe,WAAW,MAAM,eAAe,GAAG;AAC5D,UAAM,aAAa,kBAAkB,WAAW,MAAM,eAAe;AACrE,WACE,oBAAC,gBAAa,MAAM,YAAa,GAAG,YACjC,UACH;AAAA,EAEJ,OAAO;AAEL,WAAO,oBAAC,OAAG,GAAG,EAAE,MAAM,GAAG,WAAW,GAAI,UAAS;AAAA,EACnD;AACF;","names":[]}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var ScrollAfterNavigation_exports = {};
20
+ __export(ScrollAfterNavigation_exports, {
21
+ ScrollAfterNavigation: () => ScrollAfterNavigation,
22
+ useScrollAfterNavigation: () => useScrollAfterNavigation
23
+ });
24
+ module.exports = __toCommonJS(ScrollAfterNavigation_exports);
25
+ var import_react = require("react");
26
+ var import_TinkerableContext = require("../TinkerableContext");
27
+ var import_scrollToId = require("../scrollToId");
28
+ const useScrollAfterNavigation = () => {
29
+ const { navigationState } = (0, import_react.use)(import_TinkerableContext.TinkerableContext);
30
+ const frag = navigationState.hash;
31
+ const navKey = `${navigationState.sandboxPath}\0${frag}`;
32
+ (0, import_react.useEffect)(() => {
33
+ if (!frag || typeof document === "undefined") return;
34
+ if ((0, import_scrollToId.scrollToId)(frag)) return;
35
+ let done = false;
36
+ const finish = () => {
37
+ done = true;
38
+ observer.disconnect();
39
+ timers.forEach(clearTimeout);
40
+ clearTimeout(finalTimer);
41
+ };
42
+ const tryScroll = () => {
43
+ if (!done && (0, import_scrollToId.scrollToId)(frag)) finish();
44
+ };
45
+ const observer = new MutationObserver(tryScroll);
46
+ const timers = [120, 300, 600].map((ms) => setTimeout(tryScroll, ms));
47
+ const finalTimer = setTimeout(() => {
48
+ if (!done) {
49
+ finish();
50
+ window.scrollTo?.(0, 0);
51
+ }
52
+ }, 900);
53
+ observer.observe(document.body, { childList: true, subtree: true });
54
+ return finish;
55
+ }, [navKey, frag]);
56
+ };
57
+ const ScrollAfterNavigation = () => {
58
+ useScrollAfterNavigation();
59
+ return null;
60
+ };
61
+ // Annotate the CommonJS export names for ESM import in node:
62
+ 0 && (module.exports = {
63
+ ScrollAfterNavigation,
64
+ useScrollAfterNavigation
65
+ });
66
+ //# sourceMappingURL=ScrollAfterNavigation.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/ScrollAfterNavigation.tsx"],"sourcesContent":["import { use, useEffect } from 'react';\n\nimport { TinkerableContext } from '../TinkerableContext';\nimport { scrollToId } from '../scrollToId';\n\n/**\n * Deep-linking Capability C (MARKDOWN_SYNTAX_SPEC §13.5): after an in-app navigation\n * whose URL carries a `#fragment`, scroll the target section into view.\n *\n * In-app navigation swaps the rendered file **asynchronously** — the destination\n * file's tree mounts *after* the route change — so the element the fragment\n * addresses does not exist at click time. This effect records the pending fragment\n * and retries until the target appears: an immediate attempt, a `MutationObserver`\n * over late-mounting subtrees, and a few timed retries (the `[120, 300, 600]ms`\n * cadence `grove/src/components/Toc.tsx` proves for late-mounting prose). If the\n * target never appears within the window it degrades to top-of-page — a missing\n * fragment is never a hard failure.\n *\n * It re-runs when the destination page **or** the fragment changes, so a fresh click\n * on the same target re-scrolls. Mounted once inside the navigation provider (see\n * `boot`'s `TinkerableApp`) so it is uniform for **every** MDX app — the SDK router\n * owns cross-page anchor navigation, not any one consumer (Grove).\n */\nexport const useScrollAfterNavigation = (): void => {\n const { navigationState } = use(TinkerableContext);\n const frag = navigationState.hash;\n // Re-run when the destination page OR the fragment changes.\n const navKey = `${navigationState.sandboxPath}\u0000${frag}`;\n\n useEffect(() => {\n if (!frag || typeof document === 'undefined') return;\n // Fast path: the target is already in the DOM (same page, or the tree mounted\n // synchronously) — scroll now, no observer/timer churn.\n if (scrollToId(frag)) return;\n\n let done = false;\n const finish = () => {\n done = true;\n observer.disconnect();\n timers.forEach(clearTimeout);\n clearTimeout(finalTimer);\n };\n const tryScroll = () => {\n if (!done && scrollToId(frag)) finish();\n };\n\n const observer = new MutationObserver(tryScroll);\n const timers = [120, 300, 600].map((ms) => setTimeout(tryScroll, ms));\n // Final fallback once the retry window closes: if the fragment never resolved,\n // scroll to the top rather than strand the reader at the previous page's offset.\n const finalTimer = setTimeout(() => {\n if (!done) {\n finish();\n window.scrollTo?.(0, 0);\n }\n }, 900);\n\n observer.observe(document.body, { childList: true, subtree: true });\n return finish;\n }, [navKey, frag]);\n};\n\n/** Null-rendering mount point for {@link useScrollAfterNavigation} inside the\n * navigation provider. */\nexport const ScrollAfterNavigation = (): null => {\n useScrollAfterNavigation();\n return null;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA+B;AAE/B,+BAAkC;AAClC,wBAA2B;AAoBpB,MAAM,2BAA2B,MAAY;AAClD,QAAM,EAAE,gBAAgB,QAAI,kBAAI,0CAAiB;AACjD,QAAM,OAAO,gBAAgB;AAE7B,QAAM,SAAS,GAAG,gBAAgB,WAAW,KAAI,IAAI;AAErD,8BAAU,MAAM;AACd,QAAI,CAAC,QAAQ,OAAO,aAAa,YAAa;AAG9C,YAAI,8BAAW,IAAI,EAAG;AAEtB,QAAI,OAAO;AACX,UAAM,SAAS,MAAM;AACnB,aAAO;AACP,eAAS,WAAW;AACpB,aAAO,QAAQ,YAAY;AAC3B,mBAAa,UAAU;AAAA,IACzB;AACA,UAAM,YAAY,MAAM;AACtB,UAAI,CAAC,YAAQ,8BAAW,IAAI,EAAG,QAAO;AAAA,IACxC;AAEA,UAAM,WAAW,IAAI,iBAAiB,SAAS;AAC/C,UAAM,SAAS,CAAC,KAAK,KAAK,GAAG,EAAE,IAAI,CAAC,OAAO,WAAW,WAAW,EAAE,CAAC;AAGpE,UAAM,aAAa,WAAW,MAAM;AAClC,UAAI,CAAC,MAAM;AACT,eAAO;AACP,eAAO,WAAW,GAAG,CAAC;AAAA,MACxB;AAAA,IACF,GAAG,GAAG;AAEN,aAAS,QAAQ,SAAS,MAAM,EAAE,WAAW,MAAM,SAAS,KAAK,CAAC;AAClE,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,IAAI,CAAC;AACnB;AAIO,MAAM,wBAAwB,MAAY;AAC/C,2BAAyB;AACzB,SAAO;AACT;","names":[]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Deep-linking Capability C (MARKDOWN_SYNTAX_SPEC §13.5): after an in-app navigation
3
+ * whose URL carries a `#fragment`, scroll the target section into view.
4
+ *
5
+ * In-app navigation swaps the rendered file **asynchronously** — the destination
6
+ * file's tree mounts *after* the route change — so the element the fragment
7
+ * addresses does not exist at click time. This effect records the pending fragment
8
+ * and retries until the target appears: an immediate attempt, a `MutationObserver`
9
+ * over late-mounting subtrees, and a few timed retries (the `[120, 300, 600]ms`
10
+ * cadence `grove/src/components/Toc.tsx` proves for late-mounting prose). If the
11
+ * target never appears within the window it degrades to top-of-page — a missing
12
+ * fragment is never a hard failure.
13
+ *
14
+ * It re-runs when the destination page **or** the fragment changes, so a fresh click
15
+ * on the same target re-scrolls. Mounted once inside the navigation provider (see
16
+ * `boot`'s `TinkerableApp`) so it is uniform for **every** MDX app — the SDK router
17
+ * owns cross-page anchor navigation, not any one consumer (Grove).
18
+ */
19
+ declare const useScrollAfterNavigation: () => void;
20
+ /** Null-rendering mount point for {@link useScrollAfterNavigation} inside the
21
+ * navigation provider. */
22
+ declare const ScrollAfterNavigation: () => null;
23
+
24
+ export { ScrollAfterNavigation, useScrollAfterNavigation };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Deep-linking Capability C (MARKDOWN_SYNTAX_SPEC §13.5): after an in-app navigation
3
+ * whose URL carries a `#fragment`, scroll the target section into view.
4
+ *
5
+ * In-app navigation swaps the rendered file **asynchronously** — the destination
6
+ * file's tree mounts *after* the route change — so the element the fragment
7
+ * addresses does not exist at click time. This effect records the pending fragment
8
+ * and retries until the target appears: an immediate attempt, a `MutationObserver`
9
+ * over late-mounting subtrees, and a few timed retries (the `[120, 300, 600]ms`
10
+ * cadence `grove/src/components/Toc.tsx` proves for late-mounting prose). If the
11
+ * target never appears within the window it degrades to top-of-page — a missing
12
+ * fragment is never a hard failure.
13
+ *
14
+ * It re-runs when the destination page **or** the fragment changes, so a fresh click
15
+ * on the same target re-scrolls. Mounted once inside the navigation provider (see
16
+ * `boot`'s `TinkerableApp`) so it is uniform for **every** MDX app — the SDK router
17
+ * owns cross-page anchor navigation, not any one consumer (Grove).
18
+ */
19
+ declare const useScrollAfterNavigation: () => void;
20
+ /** Null-rendering mount point for {@link useScrollAfterNavigation} inside the
21
+ * navigation provider. */
22
+ declare const ScrollAfterNavigation: () => null;
23
+
24
+ export { ScrollAfterNavigation, useScrollAfterNavigation };
@@ -0,0 +1,41 @@
1
+ import { use, useEffect } from "react";
2
+ import { TinkerableContext } from "../TinkerableContext";
3
+ import { scrollToId } from "../scrollToId";
4
+ const useScrollAfterNavigation = () => {
5
+ const { navigationState } = use(TinkerableContext);
6
+ const frag = navigationState.hash;
7
+ const navKey = `${navigationState.sandboxPath}\0${frag}`;
8
+ useEffect(() => {
9
+ if (!frag || typeof document === "undefined") return;
10
+ if (scrollToId(frag)) return;
11
+ let done = false;
12
+ const finish = () => {
13
+ done = true;
14
+ observer.disconnect();
15
+ timers.forEach(clearTimeout);
16
+ clearTimeout(finalTimer);
17
+ };
18
+ const tryScroll = () => {
19
+ if (!done && scrollToId(frag)) finish();
20
+ };
21
+ const observer = new MutationObserver(tryScroll);
22
+ const timers = [120, 300, 600].map((ms) => setTimeout(tryScroll, ms));
23
+ const finalTimer = setTimeout(() => {
24
+ if (!done) {
25
+ finish();
26
+ window.scrollTo?.(0, 0);
27
+ }
28
+ }, 900);
29
+ observer.observe(document.body, { childList: true, subtree: true });
30
+ return finish;
31
+ }, [navKey, frag]);
32
+ };
33
+ const ScrollAfterNavigation = () => {
34
+ useScrollAfterNavigation();
35
+ return null;
36
+ };
37
+ export {
38
+ ScrollAfterNavigation,
39
+ useScrollAfterNavigation
40
+ };
41
+ //# sourceMappingURL=ScrollAfterNavigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/ScrollAfterNavigation.tsx"],"sourcesContent":["import { use, useEffect } from 'react';\n\nimport { TinkerableContext } from '../TinkerableContext';\nimport { scrollToId } from '../scrollToId';\n\n/**\n * Deep-linking Capability C (MARKDOWN_SYNTAX_SPEC §13.5): after an in-app navigation\n * whose URL carries a `#fragment`, scroll the target section into view.\n *\n * In-app navigation swaps the rendered file **asynchronously** — the destination\n * file's tree mounts *after* the route change — so the element the fragment\n * addresses does not exist at click time. This effect records the pending fragment\n * and retries until the target appears: an immediate attempt, a `MutationObserver`\n * over late-mounting subtrees, and a few timed retries (the `[120, 300, 600]ms`\n * cadence `grove/src/components/Toc.tsx` proves for late-mounting prose). If the\n * target never appears within the window it degrades to top-of-page — a missing\n * fragment is never a hard failure.\n *\n * It re-runs when the destination page **or** the fragment changes, so a fresh click\n * on the same target re-scrolls. Mounted once inside the navigation provider (see\n * `boot`'s `TinkerableApp`) so it is uniform for **every** MDX app — the SDK router\n * owns cross-page anchor navigation, not any one consumer (Grove).\n */\nexport const useScrollAfterNavigation = (): void => {\n const { navigationState } = use(TinkerableContext);\n const frag = navigationState.hash;\n // Re-run when the destination page OR the fragment changes.\n const navKey = `${navigationState.sandboxPath}\u0000${frag}`;\n\n useEffect(() => {\n if (!frag || typeof document === 'undefined') return;\n // Fast path: the target is already in the DOM (same page, or the tree mounted\n // synchronously) — scroll now, no observer/timer churn.\n if (scrollToId(frag)) return;\n\n let done = false;\n const finish = () => {\n done = true;\n observer.disconnect();\n timers.forEach(clearTimeout);\n clearTimeout(finalTimer);\n };\n const tryScroll = () => {\n if (!done && scrollToId(frag)) finish();\n };\n\n const observer = new MutationObserver(tryScroll);\n const timers = [120, 300, 600].map((ms) => setTimeout(tryScroll, ms));\n // Final fallback once the retry window closes: if the fragment never resolved,\n // scroll to the top rather than strand the reader at the previous page's offset.\n const finalTimer = setTimeout(() => {\n if (!done) {\n finish();\n window.scrollTo?.(0, 0);\n }\n }, 900);\n\n observer.observe(document.body, { childList: true, subtree: true });\n return finish;\n }, [navKey, frag]);\n};\n\n/** Null-rendering mount point for {@link useScrollAfterNavigation} inside the\n * navigation provider. */\nexport const ScrollAfterNavigation = (): null => {\n useScrollAfterNavigation();\n return null;\n};\n"],"mappings":"AAAA,SAAS,KAAK,iBAAiB;AAE/B,SAAS,yBAAyB;AAClC,SAAS,kBAAkB;AAoBpB,MAAM,2BAA2B,MAAY;AAClD,QAAM,EAAE,gBAAgB,IAAI,IAAI,iBAAiB;AACjD,QAAM,OAAO,gBAAgB;AAE7B,QAAM,SAAS,GAAG,gBAAgB,WAAW,KAAI,IAAI;AAErD,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,OAAO,aAAa,YAAa;AAG9C,QAAI,WAAW,IAAI,EAAG;AAEtB,QAAI,OAAO;AACX,UAAM,SAAS,MAAM;AACnB,aAAO;AACP,eAAS,WAAW;AACpB,aAAO,QAAQ,YAAY;AAC3B,mBAAa,UAAU;AAAA,IACzB;AACA,UAAM,YAAY,MAAM;AACtB,UAAI,CAAC,QAAQ,WAAW,IAAI,EAAG,QAAO;AAAA,IACxC;AAEA,UAAM,WAAW,IAAI,iBAAiB,SAAS;AAC/C,UAAM,SAAS,CAAC,KAAK,KAAK,GAAG,EAAE,IAAI,CAAC,OAAO,WAAW,WAAW,EAAE,CAAC;AAGpE,UAAM,aAAa,WAAW,MAAM;AAClC,UAAI,CAAC,MAAM;AACT,eAAO;AACP,eAAO,WAAW,GAAG,CAAC;AAAA,MACxB;AAAA,IACF,GAAG,GAAG;AAEN,aAAS,QAAQ,SAAS,MAAM,EAAE,WAAW,MAAM,SAAS,KAAK,CAAC;AAClE,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,IAAI,CAAC;AACnB;AAIO,MAAM,wBAAwB,MAAY;AAC/C,2BAAyB;AACzB,SAAO;AACT;","names":[]}
@@ -26,6 +26,7 @@ var import_react = require("react");
26
26
  var import_Link = require("./Link");
27
27
  var import_Include = require("./Include");
28
28
  var import_TinkerableContext = require("../TinkerableContext");
29
+ var import_urlUtils = require("../urlUtils");
29
30
  const labelFromTarget = (target) => {
30
31
  const base = target.split(/[\\/]/).pop() ?? target;
31
32
  return base.replace(/\.mdx?$/i, "") || target;
@@ -55,15 +56,22 @@ const WikiLink = ({
55
56
  const renderContext = (0, import_react.use)(import_Include.RenderExportedComponentContext);
56
57
  const currentFile = renderContext?.evaluationContext?.evaluation?.module?.filepath;
57
58
  const rawTarget = target ?? "";
58
- const text = children ?? label ?? (rawTarget ? labelFromTarget(rawTarget) : "");
59
+ const [pathPart, frag] = (0, import_urlUtils.splitHash)(rawTarget);
60
+ const text = children ?? label ?? (rawTarget ? labelFromTarget(pathPart || frag || rawTarget) : "");
59
61
  if (!rawTarget) {
60
62
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ir-wikilink", ...rest, children: text });
61
63
  }
62
- const resolved = resolveWikiTarget(rawTarget, currentFile);
64
+ if (pathPart === "" && frag) {
65
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Link.Link, { href: `#${frag}`, className: "ir-wikilink", "data-state": "anchor", ...rest, children: text });
66
+ }
67
+ const resolved = resolveWikiTarget(pathPart, currentFile);
63
68
  const files = filesMetadata ?? {};
64
69
  const loaded = Object.keys(files).length > 0;
65
70
  if (resolved !== void 0) {
66
71
  if (currentFile && resolved === currentFile) {
72
+ if (frag) {
73
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Link.Link, { href: `#${frag}`, className: "ir-wikilink", "data-state": "anchor", ...rest, children: text });
74
+ }
67
75
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ir-wikilink ir-wikilink-self", "data-state": "self", ...rest, children: text });
68
76
  }
69
77
  const exists = !loaded || resolved in files;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/WikiLink.tsx"],"sourcesContent":["import { ReactNode, use } from 'react';\nimport { Link } from './Link';\nimport { RenderExportedComponentContext } from './Include';\nimport { TinkerableContext } from '../TinkerableContext';\n\n/** Derive a human label from a target path: basename without the extension. */\nconst labelFromTarget = (target: string): string => {\n const base = target.split(/[\\\\/]/).pop() ?? target;\n return base.replace(/\\.mdx?$/i, '') || target;\n};\n\n/** Collapse `.`/`..`/empty segments into a clean absolute path. */\nconst normalize = (path: string): string => {\n const out: string[] = [];\n for (const seg of path.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') out.pop();\n else out.push(seg);\n }\n return '/' + out.join('/');\n};\n\n/**\n * Resolve a wiki-link target to an absolute sandbox path, or `undefined` when it\n * cannot be resolved (a relative target with no known current file). An\n * **absolute** target (`/…`) is taken verbatim; a **relative** target resolves\n * against the current file's directory. Pure path arithmetic (MARKDOWN_SYNTAX_SPEC\n * §13.2) — it never touches the filesystem or any other file.\n */\nconst resolveWikiTarget = (target: string, currentFile?: string): string | undefined => {\n if (target.startsWith('/')) return normalize(target);\n if (!currentFile) return undefined;\n const dir = currentFile.slice(0, currentFile.lastIndexOf('/'));\n return normalize(`${dir}/${target}`);\n};\n\n/**\n * Default MDX `WikiLink` component — the render target for the `[[target]]` /\n * `[[label|target]]` wiki-link syntax. The transpiler remark plugin (R3-153)\n * compiles that syntax to `<WikiLink target=\"…\" label=\"…\">`, carrying the raw\n * target/label verbatim; **resolution lives here** (MARKDOWN_SYNTAX_SPEC §13.2).\n *\n * Registered in {@link DEFAULT_MDX_COMPONENTS} so wiki-links render even in a\n * plain-markdown repo (§11.2 phantom defaults). Targets are **paths only** —\n * relative (resolved against the current file's directory) or absolute — with\n * **no implicit search path** (§13.3, a deliberate departure from Obsidian).\n *\n * The **current file** — the one the link is *authored in* — is read from the\n * ambient `<Include>` render context. Every MDX file renders through `<Include>`\n * (`FileRouter` renders even the top-level file that way), and Include publishes\n * the rendered module's `EvaluationContext` to its subtree via\n * {@link RenderExportedComponentContext}; the nearest one's\n * `evaluation.module.filepath` is this file's own `/app/…` path. Because that\n * context nests with each `<Include>`, a relative target inside an included\n * fragment resolves against the **fragment**, not the top-level page in the URL.\n *\n * The resolved path is checked for **existence** against the live metadata store\n * (keyed by absolute `/app/…` paths) for the three states (§13.3):\n * - **self** — the resolved path is the current file: inert text, no link.\n * - **broken** — no file at the resolved path (and the store has loaded): rendered\n * as marked text, **not** a throw.\n * - **resolved** — routed through {@link Link} (in-app navigation for a same-app\n * href, a plain `<a>` otherwise).\n *\n * The check is **optimistic until the metadata store loads** (an empty store never\n * flashes \"broken\"), and a relative target with no ambient render context (MDX\n * rendered outside `<Include>`) routes optimistically. Such an app overrides this\n * component (§11) for precise resolution.\n */\nexport const WikiLink = ({\n target,\n label,\n children,\n ...rest\n}: {\n target?: string;\n label?: ReactNode;\n children?: ReactNode;\n} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>): ReactNode => {\n const { filesMetadata } = use(TinkerableContext);\n const renderContext = use(RenderExportedComponentContext);\n const currentFile = renderContext?.evaluationContext?.evaluation?.module?.filepath;\n\n const rawTarget = target ?? '';\n const text = children ?? label ?? (rawTarget ? labelFromTarget(rawTarget) : '');\n\n // Defensive: the kernel never emits an empty target, but a hand-written\n // `<WikiLink>` might. Render inert text rather than a link to nowhere.\n if (!rawTarget) {\n return (\n <span className=\"ir-wikilink\" {...rest}>\n {text}\n </span>\n );\n }\n\n const resolved = resolveWikiTarget(rawTarget, currentFile);\n const files = filesMetadata ?? {};\n const loaded = Object.keys(files).length > 0;\n\n // `resolved === undefined` ⇒ a relative target with no known current file: route\n // it optimistically (can't check existence or self-ness generically).\n if (resolved !== undefined) {\n if (currentFile && resolved === currentFile) {\n return (\n <span className=\"ir-wikilink ir-wikilink-self\" data-state=\"self\" {...rest}>\n {text}\n </span>\n );\n }\n const exists = !loaded || resolved in files; // optimistic until loaded\n if (!exists) {\n return (\n <span\n className=\"ir-wikilink ir-wikilink-broken\"\n data-state=\"broken\"\n title={`No file at ${resolved}`}\n {...rest}\n >\n {text}\n </span>\n );\n }\n }\n return (\n <Link href={rawTarget} className=\"ir-wikilink\" data-state=\"resolved\" {...rest}>\n {text}\n </Link>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA0FM;AA1FN,mBAA+B;AAC/B,kBAAqB;AACrB,qBAA+C;AAC/C,+BAAkC;AAGlC,MAAM,kBAAkB,CAAC,WAA2B;AAClD,QAAM,OAAO,OAAO,MAAM,OAAO,EAAE,IAAI,KAAK;AAC5C,SAAO,KAAK,QAAQ,YAAY,EAAE,KAAK;AACzC;AAGA,MAAM,YAAY,CAAC,SAAyB;AAC1C,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,KAAK,MAAM,GAAG,GAAG;AACjC,QAAI,QAAQ,MAAM,QAAQ,IAAK;AAC/B,QAAI,QAAQ,KAAM,KAAI,IAAI;AAAA,QACrB,KAAI,KAAK,GAAG;AAAA,EACnB;AACA,SAAO,MAAM,IAAI,KAAK,GAAG;AAC3B;AASA,MAAM,oBAAoB,CAAC,QAAgB,gBAA6C;AACtF,MAAI,OAAO,WAAW,GAAG,EAAG,QAAO,UAAU,MAAM;AACnD,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,MAAM,YAAY,MAAM,GAAG,YAAY,YAAY,GAAG,CAAC;AAC7D,SAAO,UAAU,GAAG,GAAG,IAAI,MAAM,EAAE;AACrC;AAmCO,MAAM,WAAW,CAAC;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAI+E;AAC7E,QAAM,EAAE,cAAc,QAAI,kBAAI,0CAAiB;AAC/C,QAAM,oBAAgB,kBAAI,6CAA8B;AACxD,QAAM,cAAc,eAAe,mBAAmB,YAAY,QAAQ;AAE1E,QAAM,YAAY,UAAU;AAC5B,QAAM,OAAO,YAAY,UAAU,YAAY,gBAAgB,SAAS,IAAI;AAI5E,MAAI,CAAC,WAAW;AACd,WACE,4CAAC,UAAK,WAAU,eAAe,GAAG,MAC/B,gBACH;AAAA,EAEJ;AAEA,QAAM,WAAW,kBAAkB,WAAW,WAAW;AACzD,QAAM,QAAQ,iBAAiB,CAAC;AAChC,QAAM,SAAS,OAAO,KAAK,KAAK,EAAE,SAAS;AAI3C,MAAI,aAAa,QAAW;AAC1B,QAAI,eAAe,aAAa,aAAa;AAC3C,aACE,4CAAC,UAAK,WAAU,gCAA+B,cAAW,QAAQ,GAAG,MAClE,gBACH;AAAA,IAEJ;AACA,UAAM,SAAS,CAAC,UAAU,YAAY;AACtC,QAAI,CAAC,QAAQ;AACX,aACE;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,cAAW;AAAA,UACX,OAAO,cAAc,QAAQ;AAAA,UAC5B,GAAG;AAAA,UAEH;AAAA;AAAA,MACH;AAAA,IAEJ;AAAA,EACF;AACA,SACE,4CAAC,oBAAK,MAAM,WAAW,WAAU,eAAc,cAAW,YAAY,GAAG,MACtE,gBACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/WikiLink.tsx"],"sourcesContent":["import { ReactNode, use } from 'react';\nimport { Link } from './Link';\nimport { RenderExportedComponentContext } from './Include';\nimport { TinkerableContext } from '../TinkerableContext';\nimport { splitHash } from '../urlUtils';\n\n/** Derive a human label from a target path: basename without the extension. */\nconst labelFromTarget = (target: string): string => {\n const base = target.split(/[\\\\/]/).pop() ?? target;\n return base.replace(/\\.mdx?$/i, '') || target;\n};\n\n/** Collapse `.`/`..`/empty segments into a clean absolute path. */\nconst normalize = (path: string): string => {\n const out: string[] = [];\n for (const seg of path.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') out.pop();\n else out.push(seg);\n }\n return '/' + out.join('/');\n};\n\n/**\n * Resolve a wiki-link target to an absolute sandbox path, or `undefined` when it\n * cannot be resolved (a relative target with no known current file). An\n * **absolute** target (`/…`) is taken verbatim; a **relative** target resolves\n * against the current file's directory. Pure path arithmetic (MARKDOWN_SYNTAX_SPEC\n * §13.2) — it never touches the filesystem or any other file.\n */\nconst resolveWikiTarget = (target: string, currentFile?: string): string | undefined => {\n if (target.startsWith('/')) return normalize(target);\n if (!currentFile) return undefined;\n const dir = currentFile.slice(0, currentFile.lastIndexOf('/'));\n return normalize(`${dir}/${target}`);\n};\n\n/**\n * Default MDX `WikiLink` component — the render target for the `[[target]]` /\n * `[[label|target]]` wiki-link syntax. The transpiler remark plugin (R3-153)\n * compiles that syntax to `<WikiLink target=\"…\" label=\"…\">`, carrying the raw\n * target/label verbatim; **resolution lives here** (MARKDOWN_SYNTAX_SPEC §13.2).\n *\n * Registered in {@link DEFAULT_MDX_COMPONENTS} so wiki-links render even in a\n * plain-markdown repo (§11.2 phantom defaults). Targets are **paths only** —\n * relative (resolved against the current file's directory) or absolute — with\n * **no implicit search path** (§13.3, a deliberate departure from Obsidian).\n *\n * The **current file** — the one the link is *authored in* — is read from the\n * ambient `<Include>` render context. Every MDX file renders through `<Include>`\n * (`FileRouter` renders even the top-level file that way), and Include publishes\n * the rendered module's `EvaluationContext` to its subtree via\n * {@link RenderExportedComponentContext}; the nearest one's\n * `evaluation.module.filepath` is this file's own `/app/…` path. Because that\n * context nests with each `<Include>`, a relative target inside an included\n * fragment resolves against the **fragment**, not the top-level page in the URL.\n *\n * A target may carry a `#fragment` (`[[FILE.mdx#sec-8-9]]`, `[[#sec-8-9]]`): the\n * fragment is **split off** ({@link splitHash}, §13.5) and existence is resolved on\n * the **fragment-stripped path**, so a section citation to an existing file resolves\n * (not \"broken\"). The fragment then rides to navigation, where the scroll-after-nav\n * effect ({@link useScrollAfterNavigation}) lands the reader on the section.\n *\n * The resolved path is checked for **existence** against the live metadata store\n * (keyed by absolute `/app/…` paths) for the states (§13.3, §13.5):\n * - **anchor** — a fragment with no path (`[[#sec-8-9]]`), or a fragment whose path is\n * the current file: a same-page {@link Link} that scrolls in place, no route change.\n * - **self** — the resolved path is the current file (no fragment): inert text, no link.\n * - **broken** — no file at the resolved path (and the store has loaded): rendered\n * as marked text, **not** a throw.\n * - **resolved** — routed through {@link Link} (in-app navigation for a same-app\n * href, a plain `<a>` otherwise), carrying any `#fragment`.\n *\n * The check is **optimistic until the metadata store loads** (an empty store never\n * flashes \"broken\"), and a relative target with no ambient render context (MDX\n * rendered outside `<Include>`) routes optimistically. Such an app overrides this\n * component (§11) for precise resolution.\n */\nexport const WikiLink = ({\n target,\n label,\n children,\n ...rest\n}: {\n target?: string;\n label?: ReactNode;\n children?: ReactNode;\n} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>): ReactNode => {\n const { filesMetadata } = use(TinkerableContext);\n const renderContext = use(RenderExportedComponentContext);\n const currentFile = renderContext?.evaluationContext?.evaluation?.module?.filepath;\n\n const rawTarget = target ?? '';\n const [pathPart, frag] = splitHash(rawTarget);\n const text = children ?? label ?? (rawTarget ? labelFromTarget(pathPart || frag || rawTarget) : '');\n\n // Defensive: the kernel never emits an empty target, but a hand-written\n // `<WikiLink>` might. Render inert text rather than a link to nowhere.\n if (!rawTarget) {\n return (\n <span className=\"ir-wikilink\" {...rest}>\n {text}\n </span>\n );\n }\n\n // Same-page anchor: a fragment with no path (`[[#sec-8-9]]`). Scroll within the\n // current file — no route change (§13.5). <Link> intercepts a bare `#`-href.\n if (pathPart === '' && frag) {\n return (\n <Link href={`#${frag}`} className=\"ir-wikilink\" data-state=\"anchor\" {...rest}>\n {text}\n </Link>\n );\n }\n\n const resolved = resolveWikiTarget(pathPart, currentFile);\n const files = filesMetadata ?? {};\n const loaded = Object.keys(files).length > 0;\n\n // `resolved === undefined` ⇒ a relative target with no known current file: route\n // it optimistically (can't check existence or self-ness generically).\n if (resolved !== undefined) {\n if (currentFile && resolved === currentFile) {\n // The target IS this file. With a fragment it is a same-page anchor to another\n // of this file's sections; without one it is an inert self-reference.\n if (frag) {\n return (\n <Link href={`#${frag}`} className=\"ir-wikilink\" data-state=\"anchor\" {...rest}>\n {text}\n </Link>\n );\n }\n return (\n <span className=\"ir-wikilink ir-wikilink-self\" data-state=\"self\" {...rest}>\n {text}\n </span>\n );\n }\n // Existence is checked on the FRAGMENT-STRIPPED path (optimistic until loaded).\n const exists = !loaded || resolved in files;\n if (!exists) {\n return (\n <span\n className=\"ir-wikilink ir-wikilink-broken\"\n data-state=\"broken\"\n title={`No file at ${resolved}`}\n {...rest}\n >\n {text}\n </span>\n );\n }\n }\n // Resolved cross-file target: route through <Link>, carrying the raw target so its\n // `#fragment` rides through navigation to the scroll-after-nav effect (§13.5).\n return (\n <Link href={rawTarget} className=\"ir-wikilink\" data-state=\"resolved\" {...rest}>\n {text}\n </Link>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAoGM;AApGN,mBAA+B;AAC/B,kBAAqB;AACrB,qBAA+C;AAC/C,+BAAkC;AAClC,sBAA0B;AAG1B,MAAM,kBAAkB,CAAC,WAA2B;AAClD,QAAM,OAAO,OAAO,MAAM,OAAO,EAAE,IAAI,KAAK;AAC5C,SAAO,KAAK,QAAQ,YAAY,EAAE,KAAK;AACzC;AAGA,MAAM,YAAY,CAAC,SAAyB;AAC1C,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,KAAK,MAAM,GAAG,GAAG;AACjC,QAAI,QAAQ,MAAM,QAAQ,IAAK;AAC/B,QAAI,QAAQ,KAAM,KAAI,IAAI;AAAA,QACrB,KAAI,KAAK,GAAG;AAAA,EACnB;AACA,SAAO,MAAM,IAAI,KAAK,GAAG;AAC3B;AASA,MAAM,oBAAoB,CAAC,QAAgB,gBAA6C;AACtF,MAAI,OAAO,WAAW,GAAG,EAAG,QAAO,UAAU,MAAM;AACnD,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,MAAM,YAAY,MAAM,GAAG,YAAY,YAAY,GAAG,CAAC;AAC7D,SAAO,UAAU,GAAG,GAAG,IAAI,MAAM,EAAE;AACrC;AA2CO,MAAM,WAAW,CAAC;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAI+E;AAC7E,QAAM,EAAE,cAAc,QAAI,kBAAI,0CAAiB;AAC/C,QAAM,oBAAgB,kBAAI,6CAA8B;AACxD,QAAM,cAAc,eAAe,mBAAmB,YAAY,QAAQ;AAE1E,QAAM,YAAY,UAAU;AAC5B,QAAM,CAAC,UAAU,IAAI,QAAI,2BAAU,SAAS;AAC5C,QAAM,OAAO,YAAY,UAAU,YAAY,gBAAgB,YAAY,QAAQ,SAAS,IAAI;AAIhG,MAAI,CAAC,WAAW;AACd,WACE,4CAAC,UAAK,WAAU,eAAe,GAAG,MAC/B,gBACH;AAAA,EAEJ;AAIA,MAAI,aAAa,MAAM,MAAM;AAC3B,WACE,4CAAC,oBAAK,MAAM,IAAI,IAAI,IAAI,WAAU,eAAc,cAAW,UAAU,GAAG,MACrE,gBACH;AAAA,EAEJ;AAEA,QAAM,WAAW,kBAAkB,UAAU,WAAW;AACxD,QAAM,QAAQ,iBAAiB,CAAC;AAChC,QAAM,SAAS,OAAO,KAAK,KAAK,EAAE,SAAS;AAI3C,MAAI,aAAa,QAAW;AAC1B,QAAI,eAAe,aAAa,aAAa;AAG3C,UAAI,MAAM;AACR,eACE,4CAAC,oBAAK,MAAM,IAAI,IAAI,IAAI,WAAU,eAAc,cAAW,UAAU,GAAG,MACrE,gBACH;AAAA,MAEJ;AACA,aACE,4CAAC,UAAK,WAAU,gCAA+B,cAAW,QAAQ,GAAG,MAClE,gBACH;AAAA,IAEJ;AAEA,UAAM,SAAS,CAAC,UAAU,YAAY;AACtC,QAAI,CAAC,QAAQ;AACX,aACE;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,cAAW;AAAA,UACX,OAAO,cAAc,QAAQ;AAAA,UAC5B,GAAG;AAAA,UAEH;AAAA;AAAA,MACH;AAAA,IAEJ;AAAA,EACF;AAGA,SACE,4CAAC,oBAAK,MAAM,WAAW,WAAU,eAAc,cAAW,YAAY,GAAG,MACtE,gBACH;AAEJ;","names":[]}
@@ -20,13 +20,21 @@ import { ReactNode } from 'react';
20
20
  * context nests with each `<Include>`, a relative target inside an included
21
21
  * fragment resolves against the **fragment**, not the top-level page in the URL.
22
22
  *
23
+ * A target may carry a `#fragment` (`[[FILE.mdx#sec-8-9]]`, `[[#sec-8-9]]`): the
24
+ * fragment is **split off** ({@link splitHash}, §13.5) and existence is resolved on
25
+ * the **fragment-stripped path**, so a section citation to an existing file resolves
26
+ * (not "broken"). The fragment then rides to navigation, where the scroll-after-nav
27
+ * effect ({@link useScrollAfterNavigation}) lands the reader on the section.
28
+ *
23
29
  * The resolved path is checked for **existence** against the live metadata store
24
- * (keyed by absolute `/app/…` paths) for the three states (§13.3):
25
- * - **self** — the resolved path is the current file: inert text, no link.
30
+ * (keyed by absolute `/app/…` paths) for the states (§13.3, §13.5):
31
+ * - **anchor** — a fragment with no path (`[[#sec-8-9]]`), or a fragment whose path is
32
+ * the current file: a same-page {@link Link} that scrolls in place, no route change.
33
+ * - **self** — the resolved path is the current file (no fragment): inert text, no link.
26
34
  * - **broken** — no file at the resolved path (and the store has loaded): rendered
27
35
  * as marked text, **not** a throw.
28
36
  * - **resolved** — routed through {@link Link} (in-app navigation for a same-app
29
- * href, a plain `<a>` otherwise).
37
+ * href, a plain `<a>` otherwise), carrying any `#fragment`.
30
38
  *
31
39
  * The check is **optimistic until the metadata store loads** (an empty store never
32
40
  * flashes "broken"), and a relative target with no ambient render context (MDX
@@ -20,13 +20,21 @@ import { ReactNode } from 'react';
20
20
  * context nests with each `<Include>`, a relative target inside an included
21
21
  * fragment resolves against the **fragment**, not the top-level page in the URL.
22
22
  *
23
+ * A target may carry a `#fragment` (`[[FILE.mdx#sec-8-9]]`, `[[#sec-8-9]]`): the
24
+ * fragment is **split off** ({@link splitHash}, §13.5) and existence is resolved on
25
+ * the **fragment-stripped path**, so a section citation to an existing file resolves
26
+ * (not "broken"). The fragment then rides to navigation, where the scroll-after-nav
27
+ * effect ({@link useScrollAfterNavigation}) lands the reader on the section.
28
+ *
23
29
  * The resolved path is checked for **existence** against the live metadata store
24
- * (keyed by absolute `/app/…` paths) for the three states (§13.3):
25
- * - **self** — the resolved path is the current file: inert text, no link.
30
+ * (keyed by absolute `/app/…` paths) for the states (§13.3, §13.5):
31
+ * - **anchor** — a fragment with no path (`[[#sec-8-9]]`), or a fragment whose path is
32
+ * the current file: a same-page {@link Link} that scrolls in place, no route change.
33
+ * - **self** — the resolved path is the current file (no fragment): inert text, no link.
26
34
  * - **broken** — no file at the resolved path (and the store has loaded): rendered
27
35
  * as marked text, **not** a throw.
28
36
  * - **resolved** — routed through {@link Link} (in-app navigation for a same-app
29
- * href, a plain `<a>` otherwise).
37
+ * href, a plain `<a>` otherwise), carrying any `#fragment`.
30
38
  *
31
39
  * The check is **optimistic until the metadata store loads** (an empty store never
32
40
  * flashes "broken"), and a relative target with no ambient render context (MDX
@@ -3,6 +3,7 @@ import { use } from "react";
3
3
  import { Link } from "./Link";
4
4
  import { RenderExportedComponentContext } from "./Include";
5
5
  import { TinkerableContext } from "../TinkerableContext";
6
+ import { splitHash } from "../urlUtils";
6
7
  const labelFromTarget = (target) => {
7
8
  const base = target.split(/[\\/]/).pop() ?? target;
8
9
  return base.replace(/\.mdx?$/i, "") || target;
@@ -32,15 +33,22 @@ const WikiLink = ({
32
33
  const renderContext = use(RenderExportedComponentContext);
33
34
  const currentFile = renderContext?.evaluationContext?.evaluation?.module?.filepath;
34
35
  const rawTarget = target ?? "";
35
- const text = children ?? label ?? (rawTarget ? labelFromTarget(rawTarget) : "");
36
+ const [pathPart, frag] = splitHash(rawTarget);
37
+ const text = children ?? label ?? (rawTarget ? labelFromTarget(pathPart || frag || rawTarget) : "");
36
38
  if (!rawTarget) {
37
39
  return /* @__PURE__ */ jsx("span", { className: "ir-wikilink", ...rest, children: text });
38
40
  }
39
- const resolved = resolveWikiTarget(rawTarget, currentFile);
41
+ if (pathPart === "" && frag) {
42
+ return /* @__PURE__ */ jsx(Link, { href: `#${frag}`, className: "ir-wikilink", "data-state": "anchor", ...rest, children: text });
43
+ }
44
+ const resolved = resolveWikiTarget(pathPart, currentFile);
40
45
  const files = filesMetadata ?? {};
41
46
  const loaded = Object.keys(files).length > 0;
42
47
  if (resolved !== void 0) {
43
48
  if (currentFile && resolved === currentFile) {
49
+ if (frag) {
50
+ return /* @__PURE__ */ jsx(Link, { href: `#${frag}`, className: "ir-wikilink", "data-state": "anchor", ...rest, children: text });
51
+ }
44
52
  return /* @__PURE__ */ jsx("span", { className: "ir-wikilink ir-wikilink-self", "data-state": "self", ...rest, children: text });
45
53
  }
46
54
  const exists = !loaded || resolved in files;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/WikiLink.tsx"],"sourcesContent":["import { ReactNode, use } from 'react';\nimport { Link } from './Link';\nimport { RenderExportedComponentContext } from './Include';\nimport { TinkerableContext } from '../TinkerableContext';\n\n/** Derive a human label from a target path: basename without the extension. */\nconst labelFromTarget = (target: string): string => {\n const base = target.split(/[\\\\/]/).pop() ?? target;\n return base.replace(/\\.mdx?$/i, '') || target;\n};\n\n/** Collapse `.`/`..`/empty segments into a clean absolute path. */\nconst normalize = (path: string): string => {\n const out: string[] = [];\n for (const seg of path.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') out.pop();\n else out.push(seg);\n }\n return '/' + out.join('/');\n};\n\n/**\n * Resolve a wiki-link target to an absolute sandbox path, or `undefined` when it\n * cannot be resolved (a relative target with no known current file). An\n * **absolute** target (`/…`) is taken verbatim; a **relative** target resolves\n * against the current file's directory. Pure path arithmetic (MARKDOWN_SYNTAX_SPEC\n * §13.2) — it never touches the filesystem or any other file.\n */\nconst resolveWikiTarget = (target: string, currentFile?: string): string | undefined => {\n if (target.startsWith('/')) return normalize(target);\n if (!currentFile) return undefined;\n const dir = currentFile.slice(0, currentFile.lastIndexOf('/'));\n return normalize(`${dir}/${target}`);\n};\n\n/**\n * Default MDX `WikiLink` component — the render target for the `[[target]]` /\n * `[[label|target]]` wiki-link syntax. The transpiler remark plugin (R3-153)\n * compiles that syntax to `<WikiLink target=\"…\" label=\"…\">`, carrying the raw\n * target/label verbatim; **resolution lives here** (MARKDOWN_SYNTAX_SPEC §13.2).\n *\n * Registered in {@link DEFAULT_MDX_COMPONENTS} so wiki-links render even in a\n * plain-markdown repo (§11.2 phantom defaults). Targets are **paths only** —\n * relative (resolved against the current file's directory) or absolute — with\n * **no implicit search path** (§13.3, a deliberate departure from Obsidian).\n *\n * The **current file** — the one the link is *authored in* — is read from the\n * ambient `<Include>` render context. Every MDX file renders through `<Include>`\n * (`FileRouter` renders even the top-level file that way), and Include publishes\n * the rendered module's `EvaluationContext` to its subtree via\n * {@link RenderExportedComponentContext}; the nearest one's\n * `evaluation.module.filepath` is this file's own `/app/…` path. Because that\n * context nests with each `<Include>`, a relative target inside an included\n * fragment resolves against the **fragment**, not the top-level page in the URL.\n *\n * The resolved path is checked for **existence** against the live metadata store\n * (keyed by absolute `/app/…` paths) for the three states (§13.3):\n * - **self** — the resolved path is the current file: inert text, no link.\n * - **broken** — no file at the resolved path (and the store has loaded): rendered\n * as marked text, **not** a throw.\n * - **resolved** — routed through {@link Link} (in-app navigation for a same-app\n * href, a plain `<a>` otherwise).\n *\n * The check is **optimistic until the metadata store loads** (an empty store never\n * flashes \"broken\"), and a relative target with no ambient render context (MDX\n * rendered outside `<Include>`) routes optimistically. Such an app overrides this\n * component (§11) for precise resolution.\n */\nexport const WikiLink = ({\n target,\n label,\n children,\n ...rest\n}: {\n target?: string;\n label?: ReactNode;\n children?: ReactNode;\n} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>): ReactNode => {\n const { filesMetadata } = use(TinkerableContext);\n const renderContext = use(RenderExportedComponentContext);\n const currentFile = renderContext?.evaluationContext?.evaluation?.module?.filepath;\n\n const rawTarget = target ?? '';\n const text = children ?? label ?? (rawTarget ? labelFromTarget(rawTarget) : '');\n\n // Defensive: the kernel never emits an empty target, but a hand-written\n // `<WikiLink>` might. Render inert text rather than a link to nowhere.\n if (!rawTarget) {\n return (\n <span className=\"ir-wikilink\" {...rest}>\n {text}\n </span>\n );\n }\n\n const resolved = resolveWikiTarget(rawTarget, currentFile);\n const files = filesMetadata ?? {};\n const loaded = Object.keys(files).length > 0;\n\n // `resolved === undefined` ⇒ a relative target with no known current file: route\n // it optimistically (can't check existence or self-ness generically).\n if (resolved !== undefined) {\n if (currentFile && resolved === currentFile) {\n return (\n <span className=\"ir-wikilink ir-wikilink-self\" data-state=\"self\" {...rest}>\n {text}\n </span>\n );\n }\n const exists = !loaded || resolved in files; // optimistic until loaded\n if (!exists) {\n return (\n <span\n className=\"ir-wikilink ir-wikilink-broken\"\n data-state=\"broken\"\n title={`No file at ${resolved}`}\n {...rest}\n >\n {text}\n </span>\n );\n }\n }\n return (\n <Link href={rawTarget} className=\"ir-wikilink\" data-state=\"resolved\" {...rest}>\n {text}\n </Link>\n );\n};\n"],"mappings":"AA0FM;AA1FN,SAAoB,WAAW;AAC/B,SAAS,YAAY;AACrB,SAAS,sCAAsC;AAC/C,SAAS,yBAAyB;AAGlC,MAAM,kBAAkB,CAAC,WAA2B;AAClD,QAAM,OAAO,OAAO,MAAM,OAAO,EAAE,IAAI,KAAK;AAC5C,SAAO,KAAK,QAAQ,YAAY,EAAE,KAAK;AACzC;AAGA,MAAM,YAAY,CAAC,SAAyB;AAC1C,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,KAAK,MAAM,GAAG,GAAG;AACjC,QAAI,QAAQ,MAAM,QAAQ,IAAK;AAC/B,QAAI,QAAQ,KAAM,KAAI,IAAI;AAAA,QACrB,KAAI,KAAK,GAAG;AAAA,EACnB;AACA,SAAO,MAAM,IAAI,KAAK,GAAG;AAC3B;AASA,MAAM,oBAAoB,CAAC,QAAgB,gBAA6C;AACtF,MAAI,OAAO,WAAW,GAAG,EAAG,QAAO,UAAU,MAAM;AACnD,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,MAAM,YAAY,MAAM,GAAG,YAAY,YAAY,GAAG,CAAC;AAC7D,SAAO,UAAU,GAAG,GAAG,IAAI,MAAM,EAAE;AACrC;AAmCO,MAAM,WAAW,CAAC;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAI+E;AAC7E,QAAM,EAAE,cAAc,IAAI,IAAI,iBAAiB;AAC/C,QAAM,gBAAgB,IAAI,8BAA8B;AACxD,QAAM,cAAc,eAAe,mBAAmB,YAAY,QAAQ;AAE1E,QAAM,YAAY,UAAU;AAC5B,QAAM,OAAO,YAAY,UAAU,YAAY,gBAAgB,SAAS,IAAI;AAI5E,MAAI,CAAC,WAAW;AACd,WACE,oBAAC,UAAK,WAAU,eAAe,GAAG,MAC/B,gBACH;AAAA,EAEJ;AAEA,QAAM,WAAW,kBAAkB,WAAW,WAAW;AACzD,QAAM,QAAQ,iBAAiB,CAAC;AAChC,QAAM,SAAS,OAAO,KAAK,KAAK,EAAE,SAAS;AAI3C,MAAI,aAAa,QAAW;AAC1B,QAAI,eAAe,aAAa,aAAa;AAC3C,aACE,oBAAC,UAAK,WAAU,gCAA+B,cAAW,QAAQ,GAAG,MAClE,gBACH;AAAA,IAEJ;AACA,UAAM,SAAS,CAAC,UAAU,YAAY;AACtC,QAAI,CAAC,QAAQ;AACX,aACE;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,cAAW;AAAA,UACX,OAAO,cAAc,QAAQ;AAAA,UAC5B,GAAG;AAAA,UAEH;AAAA;AAAA,MACH;AAAA,IAEJ;AAAA,EACF;AACA,SACE,oBAAC,QAAK,MAAM,WAAW,WAAU,eAAc,cAAW,YAAY,GAAG,MACtE,gBACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/WikiLink.tsx"],"sourcesContent":["import { ReactNode, use } from 'react';\nimport { Link } from './Link';\nimport { RenderExportedComponentContext } from './Include';\nimport { TinkerableContext } from '../TinkerableContext';\nimport { splitHash } from '../urlUtils';\n\n/** Derive a human label from a target path: basename without the extension. */\nconst labelFromTarget = (target: string): string => {\n const base = target.split(/[\\\\/]/).pop() ?? target;\n return base.replace(/\\.mdx?$/i, '') || target;\n};\n\n/** Collapse `.`/`..`/empty segments into a clean absolute path. */\nconst normalize = (path: string): string => {\n const out: string[] = [];\n for (const seg of path.split('/')) {\n if (seg === '' || seg === '.') continue;\n if (seg === '..') out.pop();\n else out.push(seg);\n }\n return '/' + out.join('/');\n};\n\n/**\n * Resolve a wiki-link target to an absolute sandbox path, or `undefined` when it\n * cannot be resolved (a relative target with no known current file). An\n * **absolute** target (`/…`) is taken verbatim; a **relative** target resolves\n * against the current file's directory. Pure path arithmetic (MARKDOWN_SYNTAX_SPEC\n * §13.2) — it never touches the filesystem or any other file.\n */\nconst resolveWikiTarget = (target: string, currentFile?: string): string | undefined => {\n if (target.startsWith('/')) return normalize(target);\n if (!currentFile) return undefined;\n const dir = currentFile.slice(0, currentFile.lastIndexOf('/'));\n return normalize(`${dir}/${target}`);\n};\n\n/**\n * Default MDX `WikiLink` component — the render target for the `[[target]]` /\n * `[[label|target]]` wiki-link syntax. The transpiler remark plugin (R3-153)\n * compiles that syntax to `<WikiLink target=\"…\" label=\"…\">`, carrying the raw\n * target/label verbatim; **resolution lives here** (MARKDOWN_SYNTAX_SPEC §13.2).\n *\n * Registered in {@link DEFAULT_MDX_COMPONENTS} so wiki-links render even in a\n * plain-markdown repo (§11.2 phantom defaults). Targets are **paths only** —\n * relative (resolved against the current file's directory) or absolute — with\n * **no implicit search path** (§13.3, a deliberate departure from Obsidian).\n *\n * The **current file** — the one the link is *authored in* — is read from the\n * ambient `<Include>` render context. Every MDX file renders through `<Include>`\n * (`FileRouter` renders even the top-level file that way), and Include publishes\n * the rendered module's `EvaluationContext` to its subtree via\n * {@link RenderExportedComponentContext}; the nearest one's\n * `evaluation.module.filepath` is this file's own `/app/…` path. Because that\n * context nests with each `<Include>`, a relative target inside an included\n * fragment resolves against the **fragment**, not the top-level page in the URL.\n *\n * A target may carry a `#fragment` (`[[FILE.mdx#sec-8-9]]`, `[[#sec-8-9]]`): the\n * fragment is **split off** ({@link splitHash}, §13.5) and existence is resolved on\n * the **fragment-stripped path**, so a section citation to an existing file resolves\n * (not \"broken\"). The fragment then rides to navigation, where the scroll-after-nav\n * effect ({@link useScrollAfterNavigation}) lands the reader on the section.\n *\n * The resolved path is checked for **existence** against the live metadata store\n * (keyed by absolute `/app/…` paths) for the states (§13.3, §13.5):\n * - **anchor** — a fragment with no path (`[[#sec-8-9]]`), or a fragment whose path is\n * the current file: a same-page {@link Link} that scrolls in place, no route change.\n * - **self** — the resolved path is the current file (no fragment): inert text, no link.\n * - **broken** — no file at the resolved path (and the store has loaded): rendered\n * as marked text, **not** a throw.\n * - **resolved** — routed through {@link Link} (in-app navigation for a same-app\n * href, a plain `<a>` otherwise), carrying any `#fragment`.\n *\n * The check is **optimistic until the metadata store loads** (an empty store never\n * flashes \"broken\"), and a relative target with no ambient render context (MDX\n * rendered outside `<Include>`) routes optimistically. Such an app overrides this\n * component (§11) for precise resolution.\n */\nexport const WikiLink = ({\n target,\n label,\n children,\n ...rest\n}: {\n target?: string;\n label?: ReactNode;\n children?: ReactNode;\n} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>): ReactNode => {\n const { filesMetadata } = use(TinkerableContext);\n const renderContext = use(RenderExportedComponentContext);\n const currentFile = renderContext?.evaluationContext?.evaluation?.module?.filepath;\n\n const rawTarget = target ?? '';\n const [pathPart, frag] = splitHash(rawTarget);\n const text = children ?? label ?? (rawTarget ? labelFromTarget(pathPart || frag || rawTarget) : '');\n\n // Defensive: the kernel never emits an empty target, but a hand-written\n // `<WikiLink>` might. Render inert text rather than a link to nowhere.\n if (!rawTarget) {\n return (\n <span className=\"ir-wikilink\" {...rest}>\n {text}\n </span>\n );\n }\n\n // Same-page anchor: a fragment with no path (`[[#sec-8-9]]`). Scroll within the\n // current file — no route change (§13.5). <Link> intercepts a bare `#`-href.\n if (pathPart === '' && frag) {\n return (\n <Link href={`#${frag}`} className=\"ir-wikilink\" data-state=\"anchor\" {...rest}>\n {text}\n </Link>\n );\n }\n\n const resolved = resolveWikiTarget(pathPart, currentFile);\n const files = filesMetadata ?? {};\n const loaded = Object.keys(files).length > 0;\n\n // `resolved === undefined` ⇒ a relative target with no known current file: route\n // it optimistically (can't check existence or self-ness generically).\n if (resolved !== undefined) {\n if (currentFile && resolved === currentFile) {\n // The target IS this file. With a fragment it is a same-page anchor to another\n // of this file's sections; without one it is an inert self-reference.\n if (frag) {\n return (\n <Link href={`#${frag}`} className=\"ir-wikilink\" data-state=\"anchor\" {...rest}>\n {text}\n </Link>\n );\n }\n return (\n <span className=\"ir-wikilink ir-wikilink-self\" data-state=\"self\" {...rest}>\n {text}\n </span>\n );\n }\n // Existence is checked on the FRAGMENT-STRIPPED path (optimistic until loaded).\n const exists = !loaded || resolved in files;\n if (!exists) {\n return (\n <span\n className=\"ir-wikilink ir-wikilink-broken\"\n data-state=\"broken\"\n title={`No file at ${resolved}`}\n {...rest}\n >\n {text}\n </span>\n );\n }\n }\n // Resolved cross-file target: route through <Link>, carrying the raw target so its\n // `#fragment` rides through navigation to the scroll-after-nav effect (§13.5).\n return (\n <Link href={rawTarget} className=\"ir-wikilink\" data-state=\"resolved\" {...rest}>\n {text}\n </Link>\n );\n};\n"],"mappings":"AAoGM;AApGN,SAAoB,WAAW;AAC/B,SAAS,YAAY;AACrB,SAAS,sCAAsC;AAC/C,SAAS,yBAAyB;AAClC,SAAS,iBAAiB;AAG1B,MAAM,kBAAkB,CAAC,WAA2B;AAClD,QAAM,OAAO,OAAO,MAAM,OAAO,EAAE,IAAI,KAAK;AAC5C,SAAO,KAAK,QAAQ,YAAY,EAAE,KAAK;AACzC;AAGA,MAAM,YAAY,CAAC,SAAyB;AAC1C,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,KAAK,MAAM,GAAG,GAAG;AACjC,QAAI,QAAQ,MAAM,QAAQ,IAAK;AAC/B,QAAI,QAAQ,KAAM,KAAI,IAAI;AAAA,QACrB,KAAI,KAAK,GAAG;AAAA,EACnB;AACA,SAAO,MAAM,IAAI,KAAK,GAAG;AAC3B;AASA,MAAM,oBAAoB,CAAC,QAAgB,gBAA6C;AACtF,MAAI,OAAO,WAAW,GAAG,EAAG,QAAO,UAAU,MAAM;AACnD,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,MAAM,YAAY,MAAM,GAAG,YAAY,YAAY,GAAG,CAAC;AAC7D,SAAO,UAAU,GAAG,GAAG,IAAI,MAAM,EAAE;AACrC;AA2CO,MAAM,WAAW,CAAC;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,MAI+E;AAC7E,QAAM,EAAE,cAAc,IAAI,IAAI,iBAAiB;AAC/C,QAAM,gBAAgB,IAAI,8BAA8B;AACxD,QAAM,cAAc,eAAe,mBAAmB,YAAY,QAAQ;AAE1E,QAAM,YAAY,UAAU;AAC5B,QAAM,CAAC,UAAU,IAAI,IAAI,UAAU,SAAS;AAC5C,QAAM,OAAO,YAAY,UAAU,YAAY,gBAAgB,YAAY,QAAQ,SAAS,IAAI;AAIhG,MAAI,CAAC,WAAW;AACd,WACE,oBAAC,UAAK,WAAU,eAAe,GAAG,MAC/B,gBACH;AAAA,EAEJ;AAIA,MAAI,aAAa,MAAM,MAAM;AAC3B,WACE,oBAAC,QAAK,MAAM,IAAI,IAAI,IAAI,WAAU,eAAc,cAAW,UAAU,GAAG,MACrE,gBACH;AAAA,EAEJ;AAEA,QAAM,WAAW,kBAAkB,UAAU,WAAW;AACxD,QAAM,QAAQ,iBAAiB,CAAC;AAChC,QAAM,SAAS,OAAO,KAAK,KAAK,EAAE,SAAS;AAI3C,MAAI,aAAa,QAAW;AAC1B,QAAI,eAAe,aAAa,aAAa;AAG3C,UAAI,MAAM;AACR,eACE,oBAAC,QAAK,MAAM,IAAI,IAAI,IAAI,WAAU,eAAc,cAAW,UAAU,GAAG,MACrE,gBACH;AAAA,MAEJ;AACA,aACE,oBAAC,UAAK,WAAU,gCAA+B,cAAW,QAAQ,GAAG,MAClE,gBACH;AAAA,IAEJ;AAEA,UAAM,SAAS,CAAC,UAAU,YAAY;AACtC,QAAI,CAAC,QAAQ;AACX,aACE;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,cAAW;AAAA,UACX,OAAO,cAAc,QAAQ;AAAA,UAC5B,GAAG;AAAA,UAEH;AAAA;AAAA,MACH;AAAA,IAEJ;AAAA,EACF;AAGA,SACE,oBAAC,QAAK,MAAM,WAAW,WAAU,eAAc,cAAW,YAAY,GAAG,MACtE,gBACH;AAEJ;","names":[]}
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var scrollToId_exports = {};
20
+ __export(scrollToId_exports, {
21
+ scrollToId: () => scrollToId
22
+ });
23
+ module.exports = __toCommonJS(scrollToId_exports);
24
+ const scrollToId = (id) => {
25
+ if (!id || typeof document === "undefined") return false;
26
+ let el = null;
27
+ try {
28
+ el = document.getElementById(id);
29
+ if (!el) {
30
+ const escaped = typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
31
+ el = document.querySelector(`[data-slug="${escaped}"]`);
32
+ }
33
+ } catch {
34
+ return false;
35
+ }
36
+ if (el && typeof el.scrollIntoView === "function") {
37
+ el.scrollIntoView();
38
+ return true;
39
+ }
40
+ return false;
41
+ };
42
+ // Annotate the CommonJS export names for ESM import in node:
43
+ 0 && (module.exports = {
44
+ scrollToId
45
+ });
46
+ //# sourceMappingURL=scrollToId.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scrollToId.ts"],"sourcesContent":["/**\n * Scroll the element addressed by a fragment id into view (deep-linking Capability\n * C, MARKDOWN_SYNTAX_SPEC §13.5). Tries the element's own `id` first — a heading's\n * `sec-…`/text-slug id (§15) — then the `[data-slug]` fallback, so a citation that\n * used the prose text slug still lands even when the heading's own id is the\n * prose-independent `sec-…` id. Returns whether a target was found; the caller\n * decides whether to fall back to top-of-page. **Never throws** — a missing\n * fragment is a soft failure, not an error.\n */\nexport const scrollToId = (id: string): boolean => {\n if (!id || typeof document === 'undefined') return false;\n let el: Element | null = null;\n try {\n el = document.getElementById(id);\n if (!el) {\n // A citation that targeted the prose text slug still resolves via the\n // `data-slug` hook the kernel emits alongside the `sec-…` id.\n const escaped =\n typeof CSS !== 'undefined' && typeof CSS.escape === 'function'\n ? CSS.escape(id)\n : id.replace(/[\"\\\\]/g, '\\\\$&');\n el = document.querySelector(`[data-slug=\"${escaped}\"]`);\n }\n } catch {\n return false;\n }\n if (el && typeof (el as HTMLElement).scrollIntoView === 'function') {\n (el as HTMLElement).scrollIntoView();\n return true;\n }\n return false;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AASO,MAAM,aAAa,CAAC,OAAwB;AACjD,MAAI,CAAC,MAAM,OAAO,aAAa,YAAa,QAAO;AACnD,MAAI,KAAqB;AACzB,MAAI;AACF,SAAK,SAAS,eAAe,EAAE;AAC/B,QAAI,CAAC,IAAI;AAGP,YAAM,UACJ,OAAO,QAAQ,eAAe,OAAO,IAAI,WAAW,aAChD,IAAI,OAAO,EAAE,IACb,GAAG,QAAQ,UAAU,MAAM;AACjC,WAAK,SAAS,cAAc,eAAe,OAAO,IAAI;AAAA,IACxD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,MAAM,OAAQ,GAAmB,mBAAmB,YAAY;AAClE,IAAC,GAAmB,eAAe;AACnC,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Scroll the element addressed by a fragment id into view (deep-linking Capability
3
+ * C, MARKDOWN_SYNTAX_SPEC §13.5). Tries the element's own `id` first — a heading's
4
+ * `sec-…`/text-slug id (§15) — then the `[data-slug]` fallback, so a citation that
5
+ * used the prose text slug still lands even when the heading's own id is the
6
+ * prose-independent `sec-…` id. Returns whether a target was found; the caller
7
+ * decides whether to fall back to top-of-page. **Never throws** — a missing
8
+ * fragment is a soft failure, not an error.
9
+ */
10
+ declare const scrollToId: (id: string) => boolean;
11
+
12
+ export { scrollToId };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Scroll the element addressed by a fragment id into view (deep-linking Capability
3
+ * C, MARKDOWN_SYNTAX_SPEC §13.5). Tries the element's own `id` first — a heading's
4
+ * `sec-…`/text-slug id (§15) — then the `[data-slug]` fallback, so a citation that
5
+ * used the prose text slug still lands even when the heading's own id is the
6
+ * prose-independent `sec-…` id. Returns whether a target was found; the caller
7
+ * decides whether to fall back to top-of-page. **Never throws** — a missing
8
+ * fragment is a soft failure, not an error.
9
+ */
10
+ declare const scrollToId: (id: string) => boolean;
11
+
12
+ export { scrollToId };
@@ -0,0 +1,22 @@
1
+ const scrollToId = (id) => {
2
+ if (!id || typeof document === "undefined") return false;
3
+ let el = null;
4
+ try {
5
+ el = document.getElementById(id);
6
+ if (!el) {
7
+ const escaped = typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
8
+ el = document.querySelector(`[data-slug="${escaped}"]`);
9
+ }
10
+ } catch {
11
+ return false;
12
+ }
13
+ if (el && typeof el.scrollIntoView === "function") {
14
+ el.scrollIntoView();
15
+ return true;
16
+ }
17
+ return false;
18
+ };
19
+ export {
20
+ scrollToId
21
+ };
22
+ //# sourceMappingURL=scrollToId.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scrollToId.ts"],"sourcesContent":["/**\n * Scroll the element addressed by a fragment id into view (deep-linking Capability\n * C, MARKDOWN_SYNTAX_SPEC §13.5). Tries the element's own `id` first — a heading's\n * `sec-…`/text-slug id (§15) — then the `[data-slug]` fallback, so a citation that\n * used the prose text slug still lands even when the heading's own id is the\n * prose-independent `sec-…` id. Returns whether a target was found; the caller\n * decides whether to fall back to top-of-page. **Never throws** — a missing\n * fragment is a soft failure, not an error.\n */\nexport const scrollToId = (id: string): boolean => {\n if (!id || typeof document === 'undefined') return false;\n let el: Element | null = null;\n try {\n el = document.getElementById(id);\n if (!el) {\n // A citation that targeted the prose text slug still resolves via the\n // `data-slug` hook the kernel emits alongside the `sec-…` id.\n const escaped =\n typeof CSS !== 'undefined' && typeof CSS.escape === 'function'\n ? CSS.escape(id)\n : id.replace(/[\"\\\\]/g, '\\\\$&');\n el = document.querySelector(`[data-slug=\"${escaped}\"]`);\n }\n } catch {\n return false;\n }\n if (el && typeof (el as HTMLElement).scrollIntoView === 'function') {\n (el as HTMLElement).scrollIntoView();\n return true;\n }\n return false;\n};\n"],"mappings":"AASO,MAAM,aAAa,CAAC,OAAwB;AACjD,MAAI,CAAC,MAAM,OAAO,aAAa,YAAa,QAAO;AACnD,MAAI,KAAqB;AACzB,MAAI;AACF,SAAK,SAAS,eAAe,EAAE;AAC/B,QAAI,CAAC,IAAI;AAGP,YAAM,UACJ,OAAO,QAAQ,eAAe,OAAO,IAAI,WAAW,aAChD,IAAI,OAAO,EAAE,IACb,GAAG,QAAQ,UAAU,MAAM;AACjC,WAAK,SAAS,cAAc,eAAe,OAAO,IAAI;AAAA,IACxD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,MAAM,OAAQ,GAAmB,mBAAmB,YAAY;AAClE,IAAC,GAAmB,eAAe;AACnC,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
package/dist/urlUtils.cjs CHANGED
@@ -31,6 +31,7 @@ __export(urlUtils_exports, {
31
31
  parsePath: () => parsePath,
32
32
  parseTarget: () => parseTarget,
33
33
  repositoryPrefixURL: () => repositoryPrefixURL,
34
+ splitHash: () => splitHash,
34
35
  underAppRoot: () => underAppRoot
35
36
  });
36
37
  module.exports = __toCommonJS(urlUtils_exports);
@@ -45,6 +46,10 @@ const getOuterHostname = (outerHref) => {
45
46
  const getSearchParams = (search) => Object.fromEntries(
46
47
  [...new URLSearchParams(search ?? window.location.search).entries()]
47
48
  );
49
+ const splitHash = (target) => {
50
+ const i = target.indexOf("#");
51
+ return i === -1 ? [target, ""] : [target.slice(0, i), target.slice(i + 1)];
52
+ };
48
53
  const parseTarget = (target, navigation) => {
49
54
  const newNavigation = { ...navigation };
50
55
  let [prehash, hash] = target.split("#");
@@ -72,11 +77,13 @@ const repositoryPrefixURL = (outerHref, navigationState) => constructUrl(outerHr
72
77
  });
73
78
  const constructOuterUrl = (previousOuterHref, sandboxTarget, navigationState, addFilesPrefix = true) => {
74
79
  if (isAbsolutePath(sandboxTarget)) {
80
+ const [pathPart, hash] = splitHash(sandboxTarget);
75
81
  return constructUrl(
76
82
  previousOuterHref,
77
83
  {
78
84
  ...navigationState,
79
- sandboxPath: addFilesPrefix ? (0, import_pathUtils.joinPaths)(FILES_PREFIX, sandboxTarget) : sandboxTarget
85
+ sandboxPath: addFilesPrefix ? (0, import_pathUtils.joinPaths)(FILES_PREFIX, pathPart) : pathPart,
86
+ hash
80
87
  }
81
88
  );
82
89
  }
@@ -164,6 +171,7 @@ const constructUrl = (outerHref, navigationState) => {
164
171
  parsePath,
165
172
  parseTarget,
166
173
  repositoryPrefixURL,
174
+ splitHash,
167
175
  underAppRoot
168
176
  });
169
177
  //# sourceMappingURL=urlUtils.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { joinPaths } from \"./pathUtils\";\nimport { NavigationState, PathState } from \"./TinkerableContext\";\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem. The sandbox\n * fs is rooted at `/` (so apps can reach dynamic mounts like `/firestore`), with\n * the repo mounted here. URL subpaths are repo-relative, so the file router\n * resolves them under `APP_ROOT`.\n */\nexport const APP_ROOT = '/app';\n\n/** Resolve a repo-relative path (e.g. a URL subpath) to its absolute sandbox path. */\nexport const underAppRoot = (repoRelativePath: string): string =>\n joinPaths(APP_ROOT, repoRelativePath);\n\nexport const getOuterHostname = (outerHref:string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.hostname}`;\n}\n\nexport const getSearchParams = (search?: string): Record<string, string> => Object.fromEntries(\n [...(new URLSearchParams(search ?? window.location.search).entries())]);\n\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split(\"#\")\n if (prehash) {\n let [path, search] = prehash.split(\"?\")\n if (path) {\n newNavigation.sandboxPath = path\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation\n}\n\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n}\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\nexport const repositoryPrefixURL = (outerHref:string, navigationState: NavigationState) => constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: ''\n });\n\nexport const constructOuterUrl = (previousOuterHref:string, sandboxTarget:string, navigationState: NavigationState, addFilesPrefix=true):string => {\n if (isAbsolutePath(sandboxTarget)) {\n return constructUrl(\n previousOuterHref,\n {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, sandboxTarget) : sandboxTarget\n })\n }\n return (\n new URL(\n sandboxTarget,\n constructUrl(\n previousOuterHref,\n navigationState\n )\n )\n ).toString();\n}\n\nexport const isInternalHref = (outerHref:string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n}\n\nexport type PathSegment = {\n name: string,\n pattern: string,\n transform?: (pathSegment: string) => string,\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean\n}\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'namespace', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'repository', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'ref', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'sandboxPath', pattern: '.*', transform: s => `/${s}`, optionalLeadingSlash: true }\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash\n ? `(?:\\/(?<${name}>${pattern}))?`\n : `\\/(?<${name}>${pattern})`\n ).join('') +\n \"$\"\n);\n\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n}\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState\n}\n\nconst stripSlashPrefix = (s: string): string => s.startsWith('/') ? s.substring(1) : s;\n\nexport const constructUrl = (outerHref:string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name }) => {\n let value = navigationState[name];\n return stripSlashPrefix(value ?? '');\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`\n if (navigationState.search) {\n url += '?' + navigationState.search\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash\n }\n return url;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { joinPaths } from \"./pathUtils\";\nimport { NavigationState, PathState } from \"./TinkerableContext\";\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem. The sandbox\n * fs is rooted at `/` (so apps can reach dynamic mounts like `/firestore`), with\n * the repo mounted here. URL subpaths are repo-relative, so the file router\n * resolves them under `APP_ROOT`.\n */\nexport const APP_ROOT = '/app';\n\n/** Resolve a repo-relative path (e.g. a URL subpath) to its absolute sandbox path. */\nexport const underAppRoot = (repoRelativePath: string): string =>\n joinPaths(APP_ROOT, repoRelativePath);\n\nexport const getOuterHostname = (outerHref:string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.hostname}`;\n}\n\nexport const getSearchParams = (search?: string): Record<string, string> => Object.fromEntries(\n [...(new URLSearchParams(search ?? window.location.search).entries())]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split(\"#\")\n if (prehash) {\n let [path, search] = prehash.split(\"?\")\n if (path) {\n newNavigation.sandboxPath = path\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation\n}\n\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n}\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\nexport const repositoryPrefixURL = (outerHref:string, navigationState: NavigationState) => constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: ''\n });\n\nexport const constructOuterUrl = (previousOuterHref:string, sandboxTarget:string, navigationState: NavigationState, addFilesPrefix=true):string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(\n previousOuterHref,\n {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n })\n }\n return (\n new URL(\n sandboxTarget,\n constructUrl(\n previousOuterHref,\n navigationState\n )\n )\n ).toString();\n}\n\nexport const isInternalHref = (outerHref:string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n}\n\nexport type PathSegment = {\n name: string,\n pattern: string,\n transform?: (pathSegment: string) => string,\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean\n}\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'namespace', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'repository', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'ref', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'sandboxPath', pattern: '.*', transform: s => `/${s}`, optionalLeadingSlash: true }\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash\n ? `(?:\\/(?<${name}>${pattern}))?`\n : `\\/(?<${name}>${pattern})`\n ).join('') +\n \"$\"\n);\n\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n}\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState\n}\n\nconst stripSlashPrefix = (s: string): string => s.startsWith('/') ? s.substring(1) : s;\n\nexport const constructUrl = (outerHref:string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name }) => {\n let value = navigationState[name];\n return stripSlashPrefix(value ?? '');\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`\n if (navigationState.search) {\n url += '?' + navigationState.search\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash\n }\n return url;\n}\n"],"mappings":"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","names":[]}
@@ -15,6 +15,15 @@ declare const APP_ROOT = "/app";
15
15
  declare const underAppRoot: (repoRelativePath: string) => string;
16
16
  declare const getOuterHostname: (outerHref: string) => string;
17
17
  declare const getSearchParams: (search?: string) => Record<string, string>;
18
+ /**
19
+ * Split a link target into its path part and its `#fragment` (the `#` dropped),
20
+ * e.g. `"FOO.mdx#sec-8-9"` → `["FOO.mdx", "sec-8-9"]`, `"#sec-3"` → `["", "sec-3"]`,
21
+ * `"FOO.mdx"` → `["FOO.mdx", ""]`. Only the **first** `#` splits — a fragment never
22
+ * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):
23
+ * existence is resolved on the path part, the fragment is threaded to navigation.
24
+ * MARKDOWN_SYNTAX_SPEC §13.5.
25
+ */
26
+ declare const splitHash: (target: string) => [string, string];
18
27
  declare const parseTarget: (target: string, navigation: NavigationState) => NavigationState;
19
28
  declare const maybeParseUrl: (str: string) => URL | null;
20
29
  declare const isAbsolutePath: (sandboxPath: string) => boolean;
@@ -31,4 +40,4 @@ declare const parsePath: (pathname: string) => PathState;
31
40
  declare const parseHref: (href: string) => NavigationState;
32
41
  declare const constructUrl: (outerHref: string, navigationState: NavigationState) => string;
33
42
 
34
- export { APP_ROOT, FILES_PREFIX, type PathSegment, constructOuterUrl, constructUrl, getOuterHostname, getSearchParams, isAbsolutePath, isInternalHref, maybeParseUrl, parseHref, parsePath, parseTarget, repositoryPrefixURL, underAppRoot };
43
+ export { APP_ROOT, FILES_PREFIX, type PathSegment, constructOuterUrl, constructUrl, getOuterHostname, getSearchParams, isAbsolutePath, isInternalHref, maybeParseUrl, parseHref, parsePath, parseTarget, repositoryPrefixURL, splitHash, underAppRoot };
@@ -15,6 +15,15 @@ declare const APP_ROOT = "/app";
15
15
  declare const underAppRoot: (repoRelativePath: string) => string;
16
16
  declare const getOuterHostname: (outerHref: string) => string;
17
17
  declare const getSearchParams: (search?: string) => Record<string, string>;
18
+ /**
19
+ * Split a link target into its path part and its `#fragment` (the `#` dropped),
20
+ * e.g. `"FOO.mdx#sec-8-9"` → `["FOO.mdx", "sec-8-9"]`, `"#sec-3"` → `["", "sec-3"]`,
21
+ * `"FOO.mdx"` → `["FOO.mdx", ""]`. Only the **first** `#` splits — a fragment never
22
+ * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):
23
+ * existence is resolved on the path part, the fragment is threaded to navigation.
24
+ * MARKDOWN_SYNTAX_SPEC §13.5.
25
+ */
26
+ declare const splitHash: (target: string) => [string, string];
18
27
  declare const parseTarget: (target: string, navigation: NavigationState) => NavigationState;
19
28
  declare const maybeParseUrl: (str: string) => URL | null;
20
29
  declare const isAbsolutePath: (sandboxPath: string) => boolean;
@@ -31,4 +40,4 @@ declare const parsePath: (pathname: string) => PathState;
31
40
  declare const parseHref: (href: string) => NavigationState;
32
41
  declare const constructUrl: (outerHref: string, navigationState: NavigationState) => string;
33
42
 
34
- export { APP_ROOT, FILES_PREFIX, type PathSegment, constructOuterUrl, constructUrl, getOuterHostname, getSearchParams, isAbsolutePath, isInternalHref, maybeParseUrl, parseHref, parsePath, parseTarget, repositoryPrefixURL, underAppRoot };
43
+ export { APP_ROOT, FILES_PREFIX, type PathSegment, constructOuterUrl, constructUrl, getOuterHostname, getSearchParams, isAbsolutePath, isInternalHref, maybeParseUrl, parseHref, parsePath, parseTarget, repositoryPrefixURL, splitHash, underAppRoot };
package/dist/urlUtils.js CHANGED
@@ -9,6 +9,10 @@ const getOuterHostname = (outerHref) => {
9
9
  const getSearchParams = (search) => Object.fromEntries(
10
10
  [...new URLSearchParams(search ?? window.location.search).entries()]
11
11
  );
12
+ const splitHash = (target) => {
13
+ const i = target.indexOf("#");
14
+ return i === -1 ? [target, ""] : [target.slice(0, i), target.slice(i + 1)];
15
+ };
12
16
  const parseTarget = (target, navigation) => {
13
17
  const newNavigation = { ...navigation };
14
18
  let [prehash, hash] = target.split("#");
@@ -36,11 +40,13 @@ const repositoryPrefixURL = (outerHref, navigationState) => constructUrl(outerHr
36
40
  });
37
41
  const constructOuterUrl = (previousOuterHref, sandboxTarget, navigationState, addFilesPrefix = true) => {
38
42
  if (isAbsolutePath(sandboxTarget)) {
43
+ const [pathPart, hash] = splitHash(sandboxTarget);
39
44
  return constructUrl(
40
45
  previousOuterHref,
41
46
  {
42
47
  ...navigationState,
43
- sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, sandboxTarget) : sandboxTarget
48
+ sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,
49
+ hash
44
50
  }
45
51
  );
46
52
  }
@@ -127,6 +133,7 @@ export {
127
133
  parsePath,
128
134
  parseTarget,
129
135
  repositoryPrefixURL,
136
+ splitHash,
130
137
  underAppRoot
131
138
  };
132
139
  //# sourceMappingURL=urlUtils.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { joinPaths } from \"./pathUtils\";\nimport { NavigationState, PathState } from \"./TinkerableContext\";\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem. The sandbox\n * fs is rooted at `/` (so apps can reach dynamic mounts like `/firestore`), with\n * the repo mounted here. URL subpaths are repo-relative, so the file router\n * resolves them under `APP_ROOT`.\n */\nexport const APP_ROOT = '/app';\n\n/** Resolve a repo-relative path (e.g. a URL subpath) to its absolute sandbox path. */\nexport const underAppRoot = (repoRelativePath: string): string =>\n joinPaths(APP_ROOT, repoRelativePath);\n\nexport const getOuterHostname = (outerHref:string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.hostname}`;\n}\n\nexport const getSearchParams = (search?: string): Record<string, string> => Object.fromEntries(\n [...(new URLSearchParams(search ?? window.location.search).entries())]);\n\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split(\"#\")\n if (prehash) {\n let [path, search] = prehash.split(\"?\")\n if (path) {\n newNavigation.sandboxPath = path\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation\n}\n\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n}\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\nexport const repositoryPrefixURL = (outerHref:string, navigationState: NavigationState) => constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: ''\n });\n\nexport const constructOuterUrl = (previousOuterHref:string, sandboxTarget:string, navigationState: NavigationState, addFilesPrefix=true):string => {\n if (isAbsolutePath(sandboxTarget)) {\n return constructUrl(\n previousOuterHref,\n {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, sandboxTarget) : sandboxTarget\n })\n }\n return (\n new URL(\n sandboxTarget,\n constructUrl(\n previousOuterHref,\n navigationState\n )\n )\n ).toString();\n}\n\nexport const isInternalHref = (outerHref:string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n}\n\nexport type PathSegment = {\n name: string,\n pattern: string,\n transform?: (pathSegment: string) => string,\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean\n}\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'namespace', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'repository', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'ref', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'sandboxPath', pattern: '.*', transform: s => `/${s}`, optionalLeadingSlash: true }\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash\n ? `(?:\\/(?<${name}>${pattern}))?`\n : `\\/(?<${name}>${pattern})`\n ).join('') +\n \"$\"\n);\n\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n}\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState\n}\n\nconst stripSlashPrefix = (s: string): string => s.startsWith('/') ? s.substring(1) : s;\n\nexport const constructUrl = (outerHref:string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name }) => {\n let value = navigationState[name];\n return stripSlashPrefix(value ?? '');\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`\n if (navigationState.search) {\n url += '?' + navigationState.search\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash\n }\n return url;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { joinPaths } from \"./pathUtils\";\nimport { NavigationState, PathState } from \"./TinkerableContext\";\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem. The sandbox\n * fs is rooted at `/` (so apps can reach dynamic mounts like `/firestore`), with\n * the repo mounted here. URL subpaths are repo-relative, so the file router\n * resolves them under `APP_ROOT`.\n */\nexport const APP_ROOT = '/app';\n\n/** Resolve a repo-relative path (e.g. a URL subpath) to its absolute sandbox path. */\nexport const underAppRoot = (repoRelativePath: string): string =>\n joinPaths(APP_ROOT, repoRelativePath);\n\nexport const getOuterHostname = (outerHref:string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.hostname}`;\n}\n\nexport const getSearchParams = (search?: string): Record<string, string> => Object.fromEntries(\n [...(new URLSearchParams(search ?? window.location.search).entries())]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split(\"#\")\n if (prehash) {\n let [path, search] = prehash.split(\"?\")\n if (path) {\n newNavigation.sandboxPath = path\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation\n}\n\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n}\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\nexport const repositoryPrefixURL = (outerHref:string, navigationState: NavigationState) => constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: ''\n });\n\nexport const constructOuterUrl = (previousOuterHref:string, sandboxTarget:string, navigationState: NavigationState, addFilesPrefix=true):string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(\n previousOuterHref,\n {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n })\n }\n return (\n new URL(\n sandboxTarget,\n constructUrl(\n previousOuterHref,\n navigationState\n )\n )\n ).toString();\n}\n\nexport const isInternalHref = (outerHref:string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n}\n\nexport type PathSegment = {\n name: string,\n pattern: string,\n transform?: (pathSegment: string) => string,\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean\n}\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'namespace', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'repository', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'ref', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'sandboxPath', pattern: '.*', transform: s => `/${s}`, optionalLeadingSlash: true }\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash\n ? `(?:\\/(?<${name}>${pattern}))?`\n : `\\/(?<${name}>${pattern})`\n ).join('') +\n \"$\"\n);\n\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n}\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState\n}\n\nconst stripSlashPrefix = (s: string): string => s.startsWith('/') ? s.substring(1) : s;\n\nexport const constructUrl = (outerHref:string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name }) => {\n let value = navigationState[name];\n return stripSlashPrefix(value ?? '');\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`\n if (navigationState.search) {\n url += '?' + navigationState.search\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash\n }\n return url;\n}\n"],"mappings":"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","names":[]}
package/dist/version.cjs CHANGED
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  SDK_VERSION: () => SDK_VERSION
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const SDK_VERSION = "0.30.0";
24
+ const SDK_VERSION = "0.31.0";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  SDK_VERSION
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.30.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.31.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.30.0";
2
+ declare const SDK_VERSION = "0.31.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.30.0";
2
+ declare const SDK_VERSION = "0.31.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const SDK_VERSION = "0.30.0";
1
+ const SDK_VERSION = "0.31.0";
2
2
  export {
3
3
  SDK_VERSION
4
4
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.30.0';\n"],"mappings":"AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.31.0';\n"],"mappings":"AAIO,MAAM,cAAc;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immediately-run/sdk",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Runtime SDK for code executing inside an immediately.run sandbox.",
5
5
  "license": "MIT",
6
6
  "repository": "github:immediately-run/immediately-run-sdk",