>
27 | ```
28 |
29 | ```ts
30 | interface FancyIndices {
31 | [x: symbol]: number
32 | [x: `${string}Token`]: string
33 | }
34 | ```
35 |
36 | does this work?
37 |
--------------------------------------------------------------------------------
/apps/web/demos/annotations/mark/page.tsx:
--------------------------------------------------------------------------------
1 | import Content from "./content.md"
2 | import { RawCode, Pre, highlight, InnerLine } from "codehike/code"
3 | import { AnnotationHandler } from "codehike/code"
4 |
5 | export default function Page() {
6 | return
7 | }
8 |
9 | export async function Code({ codeblock }: { codeblock: RawCode }) {
10 | const highlighted = await highlight(codeblock, "github-dark")
11 | return (
12 |
17 | )
18 | }
19 | const mark: AnnotationHandler = {
20 | name: "mark",
21 | AnnotatedLine: ({ annotation, ...props }) => (
22 |
23 | ),
24 | Line: (props) => (
25 |
29 | ),
30 | }
31 |
--------------------------------------------------------------------------------
/apps/web/content/blog/fine-grained/code.tsx:
--------------------------------------------------------------------------------
1 | import { InnerToken, RawCode } from "codehike/code"
2 | import { Code as TheCode } from "@/components/code"
3 | import { AnnotationHandler, InnerLine } from "codehike/code"
4 | import { cn } from "@/lib/utils"
5 |
6 | export async function Code(props: { codeblock: RawCode; className?: string }) {
7 | return
8 | }
9 |
10 | export const fblock: AnnotationHandler = {
11 | name: "fblock",
12 | Line: ({ annotation, ...props }) => {
13 | const bg = "bg-sky-500/10"
14 | const border = "border-sky-500"
15 |
16 | return (
17 |
25 |
26 |
27 | )
28 | },
29 | }
30 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/basic.5.before-recma-compiled-jsx.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic @jsxImportSource react*/
2 | function _createMdxContent(props) {
3 | const _components = {
4 | h1: "h1",
5 | slot: "slot",
6 | ...props.components,
7 | }
8 | return (
9 | <_components.slot
10 | __hike={{
11 | children: "",
12 | title: "",
13 | _data: {
14 | header: "",
15 | },
16 | hello: "world",
17 | }}
18 | >
19 | <_components.slot path="">
20 | <_components.h1>{"Lorem"}
21 |
22 |
23 | )
24 | }
25 | export default function MDXContent(props = {}) {
26 | const { wrapper: MDXLayout } = props.components || {}
27 | return MDXLayout ? (
28 |
29 | <_createMdxContent {...props} />
30 |
31 | ) : (
32 | _createMdxContent(props)
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/basic.5.before-recma-compiled-js.js:
--------------------------------------------------------------------------------
1 | import { jsx as _jsx } from "react/jsx-runtime"
2 | function _createMdxContent(props) {
3 | const _components = {
4 | h1: "h1",
5 | slot: "slot",
6 | ...props.components,
7 | }
8 | return _jsx(_components.slot, {
9 | __hike: {
10 | children: "",
11 | title: "",
12 | _data: {
13 | header: "",
14 | },
15 | hello: "world",
16 | },
17 | children: _jsx(_components.slot, {
18 | path: "",
19 | children: _jsx(_components.h1, {
20 | children: "Lorem",
21 | }),
22 | }),
23 | })
24 | }
25 | export default function MDXContent(props = {}) {
26 | const { wrapper: MDXLayout } = props.components || {}
27 | return MDXLayout
28 | ? _jsx(MDXLayout, {
29 | ...props,
30 | children: _jsx(_createMdxContent, {
31 | ...props,
32 | }),
33 | })
34 | : _createMdxContent(props)
35 | }
36 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/code-empty.7.compiled-jsx.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic @jsxImportSource react*/
2 | /*prettier-ignore*/
3 | function _createMdxContent(props) {
4 | const {MyCode} = props.components || ({});
5 | if (!MyCode) _missingMdxReference("MyCode", true);
6 | return <>{}{"\n"} >;
11 | }
12 | export default function MDXContent(props = {}) {
13 | const { wrapper: MDXLayout } = props.components || {}
14 | return MDXLayout ? (
15 |
16 | <_createMdxContent {...props} />
17 |
18 | ) : (
19 | _createMdxContent(props)
20 | )
21 | }
22 | function _missingMdxReference(id, component) {
23 | throw new Error(
24 | "Expected " +
25 | (component ? "component" : "object") +
26 | " `" +
27 | id +
28 | "` to be defined: you likely forgot to import, pass, or provide it.",
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/apps/web/content/docs/code/link.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Link
3 | description: Add links to code snippets
4 | layout: PreviewAndImplementation
5 | ---
6 |
7 | ## !demo
8 |
9 | Add links to code snippets.
10 |
11 | **ipsum** is a link to example.com
12 |
13 | ## !implementation
14 |
15 | ```tsx code.tsx
16 | import { RawCode, Pre, highlight, AnnotationHandler } from "codehike/code"
17 |
18 | const link: AnnotationHandler = {
19 | name: "link",
20 | Inline: ({ annotation, children }) => {
21 | const { query } = annotation
22 | return {children}
23 | },
24 | }
25 |
26 | async function Code({ codeblock }: { codeblock: RawCode }) {
27 | const highlighted = await highlight(codeblock, "github-dark")
28 | return
29 | }
30 | ```
31 |
32 | ### Autolinking
33 |
34 | Instead of manually adding links to code snippets, you can use a regex to automatically link URLs.
35 |
36 |
37 |
--------------------------------------------------------------------------------
/apps/web/demos/annotations/basic/page.tsx:
--------------------------------------------------------------------------------
1 | import Content from "./content.md"
2 | import { RawCode, Pre, highlight } from "codehike/code"
3 | import { AnnotationHandler } from "codehike/code"
4 |
5 | export default function Page() {
6 | return
7 | }
8 |
9 | export async function Code({ codeblock }: { codeblock: RawCode }) {
10 | const highlighted = await highlight(codeblock, "github-dark")
11 | return (
12 |
17 | )
18 | }
19 | const borderHandler: AnnotationHandler = {
20 | name: "border",
21 | Block: ({ annotation, children }) => (
22 | {children}
23 | ),
24 | }
25 |
26 | const bgHandler: AnnotationHandler = {
27 | name: "bg",
28 | Inline: ({ annotation, children }) => (
29 | {children}
30 | ),
31 | }
32 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/code.7.compiled-jsx.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic @jsxImportSource react*/
2 | function _createMdxContent(props) {
3 | const { MyCode } = props.components || {}
4 | if (!MyCode) _missingMdxReference("MyCode", true)
5 | return (
6 |
13 | )
14 | }
15 | export default function MDXContent(props = {}) {
16 | const { wrapper: MDXLayout } = props.components || {}
17 | return MDXLayout ? (
18 |
19 | <_createMdxContent {...props} />
20 |
21 | ) : (
22 | _createMdxContent(props)
23 | )
24 | }
25 | function _missingMdxReference(id, component) {
26 | throw new Error(
27 | "Expected " +
28 | (component ? "component" : "object") +
29 | " `" +
30 | id +
31 | "` to be defined: you likely forgot to import, pass, or provide it.",
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/basic.5.before-recma-compiled-function.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic @jsxImportSource react*/
2 | "use strict"
3 | function _createMdxContent(props) {
4 | const _components = {
5 | h1: "h1",
6 | slot: "slot",
7 | ...props.components,
8 | }
9 | return (
10 | <_components.slot
11 | __hike={{
12 | children: "",
13 | title: "",
14 | _data: {
15 | header: "",
16 | },
17 | hello: "world",
18 | }}
19 | >
20 | <_components.slot path="">
21 | <_components.h1>{"Lorem"}
22 |
23 |
24 | )
25 | }
26 | function MDXContent(props = {}) {
27 | const { wrapper: MDXLayout } = props.components || {}
28 | return MDXLayout ? (
29 |
30 | <_createMdxContent {...props} />
31 |
32 | ) : (
33 | _createMdxContent(props)
34 | )
35 | }
36 | return {
37 | default: MDXContent,
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/components/annotations/pill.tsx:
--------------------------------------------------------------------------------
1 | import { AnnotationHandler } from "codehike/code"
2 |
3 | const colors = [
4 | "bg-green-500/20",
5 | "bg-teal-500/20",
6 | "bg-sky-500/20",
7 | "bg-violet-500/20",
8 | "bg-fuchsia-500/20",
9 | "bg-pink-500/20",
10 | // if adding more colors, dont forget to update global.css
11 | ]
12 |
13 | export const pill: AnnotationHandler = {
14 | name: "pill",
15 | Inline: ({ annotation, children }) => {
16 | const n = Number(annotation.query || "1")
17 | const bg = colors[n % colors.length]
18 | return {children}
19 | },
20 | }
21 |
22 | export function Pill({
23 | children,
24 | n = 1,
25 | }: {
26 | children: React.ReactNode
27 | n: number
28 | }) {
29 | const bg = colors[n % colors.length]
30 | return (
31 |
35 | {children}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/empty-line.7.compiled-jsx.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic @jsxImportSource react*/
2 | /*prettier-ignore*/
3 | function _createMdxContent(props) {
4 | const {MyCode} = props.components || ({});
5 | if (!MyCode) _missingMdxReference("MyCode", true);
6 | return <>{}{"\n"} >;
11 | }
12 | export default function MDXContent(props = {}) {
13 | const { wrapper: MDXLayout } = props.components || {}
14 | return MDXLayout ? (
15 |
16 | <_createMdxContent {...props} />
17 |
18 | ) : (
19 | _createMdxContent(props)
20 | )
21 | }
22 | function _missingMdxReference(id, component) {
23 | throw new Error(
24 | "Expected " +
25 | (component ? "component" : "object") +
26 | " `" +
27 | id +
28 | "` to be defined: you likely forgot to import, pass, or provide it.",
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/apps/web/components/annotations/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { AnnotationHandler, InlineAnnotationComponent } from "codehike/code"
2 | import {
3 | TooltipProvider,
4 | Tooltip,
5 | TooltipTrigger,
6 | TooltipContent,
7 | TooltipArrow,
8 | } from "@/components/ui/tooltip"
9 |
10 | export const tooltip: AnnotationHandler = {
11 | name: "tooltip",
12 | Inline: ({ children, annotation }) => {
13 | const { query, data } = annotation
14 | return (
15 |
16 |
17 |
18 | {children}
19 |
20 |
26 | {data?.children || query}
27 |
28 |
29 |
30 | )
31 | },
32 | }
33 |
--------------------------------------------------------------------------------
/.github/scripts/website-pr.mjs:
--------------------------------------------------------------------------------
1 | import { Octokit } from "@octokit/action"
2 | import github from "@actions/github"
3 | import { BASE_BRANCH } from "./params.mjs"
4 |
5 | const octokit = new Octokit({})
6 |
7 | console.log("Find existing PR")
8 | const { data: prs } = await octokit.pulls.list({
9 | ...github.context.repo,
10 | state: "open",
11 | base: "main",
12 | head: `${github.context.repo.owner}:${BASE_BRANCH}`,
13 | })
14 | console.log("Existing PRs", prs)
15 |
16 | const title = `✨ Update website ✨`
17 | const body = ""
18 |
19 | if (prs.length === 0) {
20 | console.log("Creating new PR")
21 | await octokit.rest.pulls.create({
22 | ...github.context.repo,
23 | base: "main",
24 | head: BASE_BRANCH,
25 | title,
26 | body,
27 | })
28 | } else {
29 | // console.log("Updating existing PR")
30 | // const { number } = prs[0]
31 | // await octokit.rest.pulls.update({
32 | // ...github.context.repo,
33 | // pull_number: number,
34 | // title,
35 | // body,
36 | // })
37 | }
38 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/indentation.9.rendered.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | if
4 | (
5 | true
6 | )
7 |
8 |
9 | return
10 | 3
11 |
12 |
13 |
14 |
15 |
16 | if
17 | True
18 | :
19 |
20 |
21 | return
22 | 1
23 |
24 |
25 | return
26 | 2
27 |
28 |
29 |
--------------------------------------------------------------------------------
/apps/web/app/landing/demo.md:
--------------------------------------------------------------------------------
1 | ```jsx !page page.jsx
2 | import Content from "./content.md"
3 | import { parse } from "codehike"
4 |
5 | // !rainbow(1:2)
6 | // extract structured content:
7 | // !tt[/content/] foo
8 | const content = parse(Content)
9 |
10 | export function Page() {
11 | // !rainbow(3:5) 1
12 | return (
13 |
14 | {/* render it as you want: */}
15 |
16 |
17 |
18 | )
19 | }
20 | ```
21 |
22 | ```jsx !content
23 | content = {
24 | // !block(1:4) 1
25 | intro: {
26 | title: "Roman Emperors",
27 | children: The ...
,
28 | },
29 | emperors: [
30 | // !block(1:9) 2
31 | {
32 | title: "Augustus",
33 | children: The ...
,
34 | img: { src: "/one.png" },
35 | code: {
36 | lang: "js",
37 | value: "console.log(1)",
38 | },
39 | },
40 | // !block 3
41 | { title: "Nero", ... },
42 | // !block 4
43 | { title: "Trajan", ... },
44 | ],
45 | }
46 | ```
47 |
--------------------------------------------------------------------------------
/apps/web/demos/token-transitions/code.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React from "react"
4 | import { HighlightedCode } from "codehike/code"
5 | import { Pre } from "codehike/code"
6 | import { tokenTransitions } from "@/components/annotations/token-transitions"
7 |
8 | export function CodeSwitcher({ infos }: { infos: HighlightedCode[] }) {
9 | const [index, setIndex] = React.useState(0)
10 | const next = () => setIndex((index + 1) % infos.length)
11 |
12 | return (
13 | <>
14 |
15 |
16 |
17 | Switch code
18 |
19 |
20 | >
21 | )
22 | }
23 |
24 | export function CodeClient(props: { highlighted: HighlightedCode }) {
25 | const { highlighted } = props
26 | return (
27 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/apps/web/content/blog/codeblocks.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Perfect codeblocks
3 | description: All the small details that make flawless codeblocks.
4 | date: 2024-01-02
5 | authors: [pomber]
6 | draft: true
7 | ---
8 |
9 | Lorem ipsum
10 |
11 | ```js
12 | const a = 1
13 |
14 | // !Collapse(1:3) collapsed
15 | function foo() {
16 | let b = 2
17 | return a + b
18 | }
19 |
20 | // !Collapse(1:3)
21 | function bar() {
22 | let b = 2
23 | return a + b
24 | }
25 |
26 | console.log(foo())
27 | ```
28 |
29 | Lorem ipsum
30 |
31 | Lorem ipsum
32 |
33 | Lorem ipsum
34 |
35 | Lorem ipsum
36 |
37 | Lorem ipsum
38 |
39 | Lorem ipsum
40 |
41 | Lorem ipsum
42 |
43 | Lorem ipsum
44 |
45 | Lorem ipsum
46 |
47 | Lorem ipsum
48 |
49 | Lorem ipsum
50 |
51 | Lorem ipsum
52 |
53 | Lorem ipsum
54 |
55 | Lorem ipsum
56 |
57 | Lorem ipsum
58 |
59 | Lorem ipsum
60 |
61 | Lorem ipsum
62 |
63 | Lorem ipsum
64 |
65 | Lorem ipsum
66 |
67 | Lorem ipsum
68 |
69 | Lorem ipsum
70 |
71 | Lorem ipsum
72 |
73 | Lorem ipsum
74 |
75 | Lorem ipsum
76 |
77 | Lorem ipsum
78 |
79 | Lorem ipsum
80 |
--------------------------------------------------------------------------------
/apps/web/components/annotations/ruler.tsx:
--------------------------------------------------------------------------------
1 | import { AnnotationHandler } from "codehike/code"
2 |
3 | const colors = [
4 | "bg-green-500/20",
5 | "bg-teal-500/20",
6 | "bg-sky-500/20",
7 | "bg-violet-500/20",
8 | "bg-fuchsia-500/20",
9 | "bg-pink-500/20",
10 | // if adding more colors, dont forget to update global.css
11 | ]
12 |
13 | // needs ruler-group class in container
14 |
15 | export const ruler: AnnotationHandler = {
16 | name: "ruler",
17 | Block: ({ annotation, children }) => {
18 | const [k, c] = annotation.query?.split(" ")
19 | const n = Number(k || "1") % colors.length
20 | const bg = colors[n]
21 | return (
22 |
23 |
27 |
30 |
{children}
31 |
32 | )
33 | },
34 | }
35 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/empty-children-and-import.7.compiled-jsx.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic @jsxImportSource react*/
2 | import { x } from "@/components/code/code-with-notes"
3 | function _createMdxContent(props) {
4 | const _components = {
5 | p: "p",
6 | slot: "slot",
7 | ...props.components,
8 | }
9 | const _blocks = {
10 | children: undefined,
11 | title: "",
12 | _data: {
13 | header: "",
14 | },
15 | demo: {
16 | children: (
17 | <_components.p>{"Add callouts inside your code blocks."}
18 | ),
19 | title: "",
20 | _data: {
21 | header: "## !demo",
22 | },
23 | },
24 | }
25 | if (props._returnBlocks) {
26 | return _blocks
27 | }
28 | return _blocks.children
29 | }
30 | export default function MDXContent(props = {}) {
31 | const { wrapper: MDXLayout } = props.components || {}
32 | return MDXLayout ? (
33 |
34 | <_createMdxContent {...props} />
35 |
36 | ) : (
37 | _createMdxContent(props)
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/apps/web/components/all-code-demos.tsx:
--------------------------------------------------------------------------------
1 | import { docs } from "@/app/source"
2 | import { Block, parseRoot } from "codehike/blocks"
3 | import { Demo } from "@/components/demo"
4 | import { CodeWithNotes } from "@/components/code/code-with-notes"
5 | import Link from "next/link"
6 |
7 | export function AllCodeDemos() {
8 | const p = docs.getPages()
9 | const codePages = p.filter((page) => page.slugs[0] === "code")
10 | const demoPages = codePages.filter(
11 | (page) => page.data.layout === "PreviewAndImplementation",
12 | )
13 |
14 | return demoPages.map((page) => {
15 | const { title, exports } = page.data
16 | const { default: MDX } = exports
17 | const { demo } = parseRoot(MDX, Block.extend({ demo: Block }), {
18 | components: { Demo, CodeWithNotes },
19 | })
20 | const href = `/docs/${page.slugs.join("/")}`
21 |
22 | return (
23 |
24 |
{title}
25 | {demo.children}
26 |
27 | See {title} implementation.
28 |
29 |
30 | )
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/apps/web/demos/annotations/query/page.tsx:
--------------------------------------------------------------------------------
1 | import Content from "./content.md"
2 | import { RawCode, Pre, highlight } from "codehike/code"
3 | import { AnnotationHandler } from "codehike/code"
4 |
5 | export default function Page() {
6 | return
7 | }
8 |
9 | export async function Code({ codeblock }: { codeblock: RawCode }) {
10 | const highlighted = await highlight(codeblock, "github-dark")
11 | return (
12 |
17 | )
18 | }
19 | const borderHandler: AnnotationHandler = {
20 | name: "border",
21 | Block: ({ annotation, children }) => {
22 | const borderColor = annotation.query || "red"
23 | return {children}
24 | },
25 | }
26 |
27 | const bgHandler: AnnotationHandler = {
28 | name: "bg",
29 | Inline: ({ annotation, children }) => {
30 | const background = annotation.query || "#2d26"
31 | return {children}
32 | },
33 | }
34 |
--------------------------------------------------------------------------------
/apps/web/content/docs/code/classname.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: ClassName
3 | description: Add a class name to a piece of code
4 | layout: PreviewAndImplementation
5 | ---
6 |
7 | ## !demo
8 |
9 | Add a class name to a piece of code.
10 |
11 |
12 |
13 | ## !implementation
14 |
15 | ```tsx classname.tsx -c
16 | import { AnnotationHandler } from "codehike/code"
17 |
18 | export const className: AnnotationHandler = {
19 | name: "className",
20 | Block: ({ annotation, children }) => (
21 | {children}
22 | ),
23 | Inline: ({ annotation, children }) => (
24 | {children}
25 | ),
26 | }
27 | ```
28 |
29 | And then add the handler to your `Code` component:
30 |
31 | ```tsx code.tsx -c
32 | import { RawCode, Pre, highlight } from "codehike/code"
33 | import { className } from "./classname"
34 |
35 | async function Code({ codeblock }: { codeblock: RawCode }) {
36 | const highlighted = await highlight(codeblock, "github-dark")
37 | return
38 | }
39 | ```
40 |
--------------------------------------------------------------------------------
/apps/web/demos/line-numbers/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | RawCode,
3 | Pre,
4 | highlight,
5 | AnnotationHandler,
6 | InnerLine,
7 | } from "codehike/code"
8 | import Content from "./content.md"
9 |
10 | export default function Page() {
11 | return
12 | }
13 |
14 | async function Code({ codeblock }: { codeblock: RawCode }) {
15 | const info = await highlight(codeblock, "github-dark")
16 |
17 | return (
18 |
23 | )
24 | }
25 |
26 | export const lineNumbers: AnnotationHandler = {
27 | name: "line-numbers",
28 | Line: (props) => {
29 | const width = props.totalLines.toString().length + 1
30 | return (
31 |
32 |
36 | {props.lineNumber}
37 |
38 |
39 |
40 | )
41 | },
42 | }
43 |
--------------------------------------------------------------------------------
/apps/web/app/blog/feed.xml/route.ts:
--------------------------------------------------------------------------------
1 | import RSS from "rss"
2 | import { blog } from "../../source"
3 |
4 | const posts = blog.getPages().filter((page) => page.data.draft !== true)
5 |
6 | export const dynamic = "force-static"
7 |
8 | export async function GET() {
9 | const feed = new RSS({
10 | title: "Code Hike",
11 | description: "Markdown, React and everything in between.",
12 | generator: "RSS for Node and Next.js",
13 | feed_url: "https://codehike.org/blog/feed.xml",
14 | site_url: "https://codehike.org/blog",
15 | language: "en-US",
16 | pubDate: new Date().toUTCString(),
17 | ttl: 120,
18 | image_url: "https://codehike.org/logo.png",
19 | })
20 |
21 | posts.map(({ data, url }) => {
22 | feed.item({
23 | title: data.title,
24 | description: data.description || "",
25 | url: `https://codehike.org${url}`,
26 | author: data.authors[0],
27 | date: data.date,
28 | })
29 | })
30 |
31 | return new Response(feed.xml({ indent: true }), {
32 | headers: {
33 | "Content-Type": "application/atom+xml; charset=utf-8",
34 | },
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/.github/workflows/pr-merged.yml:
--------------------------------------------------------------------------------
1 | name: PR Merged
2 |
3 | on:
4 | pull_request_target:
5 | types: [closed]
6 | branches:
7 | - next
8 |
9 | jobs:
10 | comment-issues:
11 | name: Comment Issues
12 | if: >
13 | github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'changeset')
14 |
15 | runs-on: ubuntu-latest
16 |
17 | permissions:
18 | issues: write
19 | pull-requests: write
20 |
21 | steps:
22 | - name: Checkout code
23 | uses: actions/checkout@v4
24 | with:
25 | ref: "${{ github.event.pull_request.merge_commit_sha }}"
26 |
27 | - name: Install pnpm
28 | uses: pnpm/action-setup@v4
29 | with:
30 | run_install: false
31 |
32 | - name: Set up Node.js
33 | uses: actions/setup-node@v4
34 | with:
35 | node-version: 20
36 | cache: "pnpm"
37 |
38 | - run: pnpm install
39 |
40 | - name: Add comment to issues
41 | run: node .github/scripts/pr-merged.mjs
42 | env:
43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 |
--------------------------------------------------------------------------------
/apps/web/demos/slideshow/controls.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { useSelectedIndex } from "codehike/utils/selection"
3 |
4 | export function Controls({ length }: { length: number }) {
5 | const [selectedIndex, setSelectedIndex] =
6 | useSelectedIndex()
7 |
8 | return (
9 |
10 |
13 | setSelectedIndex(Math.max(0, selectedIndex - 1))
14 | }
15 | >
16 | Prev
17 |
18 | {[...Array(length)].map((_, i) => (
19 | setSelectedIndex(i)}
25 | />
26 | ))}
27 |
30 | setSelectedIndex(
31 | Math.min(length - 1, selectedIndex + 1),
32 | )
33 | }
34 | >
35 | Next
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Code Hike
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/_readme.md:
--------------------------------------------------------------------------------
1 | # MD Test Suite
2 |
3 | All the `*.0.mdx` files in this directory are tests
4 |
5 | Add the snapshots you are interested in testing to the `snapshots` array in the frontmatter of the mdx file:
6 |
7 | ```md
8 | ---
9 | syntaxHighlight: github-light
10 | snapshots:
11 | - before-remark
12 | - after-remark
13 | - after-rehype
14 | - before-recma-compiled-js
15 | - before-recma-compiled-jsx
16 | - before-recma-compiled-function
17 | - before-recma-js
18 | - before-recma-js-dev
19 | - before-recma-jsx
20 | - after-recma-js
21 | - after-recma-js-dev
22 | - after-recma-jsx
23 | - compiled-js
24 | - compiled-js-dev
25 | - compiled-jsx
26 | - compiled-function
27 | - parsed-jsx
28 | - rendered
29 | - rendered-dev
30 | ---
31 | ```
32 |
33 | ## Errors
34 |
35 | If there are errors in the test, it will be snapshoted in the `testname.1.error.md` file
36 |
37 | ## Running only one test:
38 |
39 | filter by test name: `pnpm watch -t foo` and then `r` to rerun the current test
40 |
41 | ## TODO
42 |
43 | - rerun tests when md file changes
44 | - remove carriage returns from snapshots
45 |
--------------------------------------------------------------------------------
/.github/workflows/release-pr-merged.yml:
--------------------------------------------------------------------------------
1 | name: Release PR Merged
2 |
3 | on:
4 | pull_request:
5 | types: [closed]
6 | branches:
7 | - next
8 |
9 | jobs:
10 | release:
11 | name: Release
12 | if: >
13 | github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
14 |
15 | runs-on: ubuntu-latest
16 |
17 | permissions:
18 | issues: write
19 | pull-requests: write
20 | contents: write
21 |
22 | steps:
23 | - name: Checkout code
24 | uses: actions/checkout@v4
25 |
26 | - name: Install pnpm
27 | uses: pnpm/action-setup@v4
28 | with:
29 | run_install: false
30 |
31 | - name: Set up Node.js
32 | uses: actions/setup-node@v4
33 | with:
34 | node-version: 20
35 | cache: "pnpm"
36 |
37 | - run: pnpm install
38 |
39 | - run: pnpm build
40 |
41 | - name: Release and update issues comments
42 | run: node .github/scripts/release.mjs
43 | env:
44 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 |
--------------------------------------------------------------------------------
/packages/codehike/src/mdx.ts:
--------------------------------------------------------------------------------
1 | import { Plugin } from "unified"
2 | // this adds the jsx node types to Root
3 | import "mdast-util-mdx-jsx"
4 | import { Root } from "mdast"
5 |
6 | import { transformImportedCode } from "./mdx/0.import-code-from-path.js"
7 | import { transformAllHikes } from "./mdx/1.0.transform-hikes.js"
8 | import { transformAllCode } from "./mdx/2.transform-code.js"
9 | import { transformHikeProps } from "./mdx/3.transform-hike-props.js"
10 | import { CodeHikeConfig } from "./mdx/config.js"
11 |
12 | export type { CodeHikeConfig }
13 |
14 | export const remarkCodeHike: Plugin<[CodeHikeConfig?], Root, Root> = (
15 | config,
16 | ) => {
17 | const safeConfig = config || {}
18 | return async (root, file) => {
19 | let tree = await transformImportedCode(root, file)
20 | tree = await transformAllHikes(tree, safeConfig)
21 | tree = await transformAllCode(tree, safeConfig)
22 | return tree
23 | }
24 | }
25 |
26 | export const recmaCodeHike: Plugin<[CodeHikeConfig?], Root, Root> = (
27 | config,
28 | ) => {
29 | return async (root) => {
30 | let tree = transformHikeProps(root) as any
31 | return tree as any
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/apps/web/app/demo/content.mdx:
--------------------------------------------------------------------------------
1 | normal, extra spacer right
2 |
3 | ```js
4 | function lorem(ipsum, dolor = 1) {
5 | // !mark
6 | dolor = sit - amet(dolor)
7 | dolor = sit - amet(dolor)
8 | dolor = sit - amet(dolor)
9 | dolor = sit - amet(dolor)
10 | // !mark
11 | dolor = sit - amet(dolor)
12 | dolor = sit - amet(dolor)
13 | // !diff +
14 | dolor = sit - amet(dolor)
15 | dolor = sit - amet(dolor)
16 | dolor = sit - amet(dolor)
17 | dolor = sit - amet(dolor)
18 | // !collapse(1:3)
19 | dolor = sit - amet(dolor)
20 | // !callout[/sit/] hey
21 | dolor = sit - amet(dolor)
22 | dolor = sit - amet(dolor)
23 | }
24 | ```
25 |
26 |
27 |
28 | overflow
29 |
30 | ```js
31 | function lorem(ipsum, dolor = 1) {
32 | // !mark
33 | dolor = sit
34 | }
35 | ```
36 |
37 |
38 |
39 |
40 |
41 | wrap
42 |
43 | ```js w
44 | // !callout[/lorem/] hey
45 | function lorem(ipsum) {
46 | // !mark
47 | dolor = sit - amet(dolor) - consectetur(ipsum)
48 | // !callout[/dolor/] hey
49 | if (dolor === 1) {
50 | // !callout[/sit/] hey
51 | dolor = sit
52 | }
53 | }
54 | ```
55 |
56 |
57 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/stacked-token.9.rendered.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | const
4 | a
5 | =
6 | 1
7 |
8 |
9 | const
10 | b
11 | =
12 | 2
13 |
14 |
15 |
16 | const
17 |
18 |
19 | c
20 |
21 |
22 | =
23 |
24 |
25 | 3
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/packages/codehike/src/code.tsx:
--------------------------------------------------------------------------------
1 | import type {
2 | Token,
3 | RawCode,
4 | HighlightedCode,
5 | CodeAnnotation,
6 | Theme,
7 | InlineAnnotation,
8 | BlockAnnotation,
9 | InlineAnnotationComponent,
10 | BlockAnnotationComponent,
11 | AnnotationHandler,
12 | CustomPreProps,
13 | CustomPre,
14 | CustomLine,
15 | CustomLineWithAnnotation,
16 | CustomToken,
17 | CustomTokenWithAnnotation,
18 | InlineProps,
19 | } from "./code/types.js"
20 |
21 | import { highlight } from "./code/highlight.js"
22 | import { Pre, Inline } from "./code/pre.js"
23 | import { InnerPre, getPreRef, InnerLine, InnerToken } from "./code/inner.js"
24 |
25 | export type {
26 | RawCode,
27 | HighlightedCode,
28 | Token,
29 | InlineAnnotation,
30 | BlockAnnotation,
31 | CodeAnnotation,
32 | // AnnotationHandler:
33 | AnnotationHandler,
34 | InlineProps,
35 | CustomPre,
36 | CustomPreProps,
37 | BlockAnnotationComponent,
38 | CustomLine,
39 | CustomLineWithAnnotation,
40 | InlineAnnotationComponent,
41 | CustomToken,
42 | CustomTokenWithAnnotation,
43 | Theme,
44 | }
45 |
46 | export { highlight, Pre, InnerPre, InnerLine, InnerToken, getPreRef, Inline }
47 |
--------------------------------------------------------------------------------
/.github/workflows/pr-updated.yml:
--------------------------------------------------------------------------------
1 | name: PR Updated
2 |
3 | on:
4 | pull_request_target:
5 | branches:
6 | - next
7 |
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
10 | cancel-in-progress: true
11 |
12 | jobs:
13 | comment-pr:
14 | name: Comment PR
15 | runs-on: ubuntu-latest
16 |
17 | permissions:
18 | issues: write
19 | pull-requests: write
20 |
21 | steps:
22 | - name: Checkout code
23 | uses: actions/checkout@v4
24 | with:
25 | ref: "${{ github.event.pull_request.head.sha }}"
26 |
27 | - name: Install pnpm
28 | uses: pnpm/action-setup@v4
29 | with:
30 | run_install: false
31 |
32 | - name: Set up Node.js
33 | uses: actions/setup-node@v4
34 | with:
35 | node-version: 20
36 | cache: "pnpm"
37 |
38 | - run: pnpm install
39 |
40 | - run: pnpm build
41 |
42 | - run: pnpm canary
43 |
44 | - name: Add or update PR comment
45 | run: node .github/scripts/pr-updated.mjs ${{ github.event.pull_request.number }}
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 |
--------------------------------------------------------------------------------
/apps/web/content/blog/remotion/01.jsx:
--------------------------------------------------------------------------------
1 | import Content from "./content.md"
2 |
3 | // !mark(1:11)
4 | // !focus(1:11)
5 | const Schema = Block.extend({
6 | steps: z.array(
7 | Block.extend({
8 | code: HighlightedCodeBlock,
9 | }),
10 | ),
11 | })
12 | const { steps } = parseRoot(
13 | Content,
14 | Schema,
15 | )
16 |
17 | const STEP_FRAMES = 60
18 | function Video({ steps }) {
19 | return (
20 |
26 | {steps.map((step, i) => (
27 |
33 |
34 |
35 | ))}
36 |
37 | )
38 | }
39 |
40 | export function RemotionRoot() {
41 | return (
42 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/apps/web/components/layout-demo.tsx:
--------------------------------------------------------------------------------
1 | import fs from "fs"
2 | import path from "path"
3 | import { Tab, Tabs } from "next-docs-ui/components/tabs"
4 | import { Code } from "./code"
5 |
6 | export async function LayoutDemo({
7 | name,
8 | children,
9 | content = "content.md",
10 | }: {
11 | name: string
12 | content?: string
13 | children: React.ReactNode
14 | }) {
15 | const value = await fs.promises.readFile(
16 | path.join(process.cwd(), "demos", name, content),
17 | "utf-8",
18 | )
19 |
20 | const { default: Page } = await import(`@/demos/${name}/page`)
21 |
22 | return (
23 |
24 |
28 |
31 |
32 |
33 |
40 |
41 | {children}
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/apps/web/content/docs/examples.tsx:
--------------------------------------------------------------------------------
1 | import { Block, ImageBlock, parseProps } from "codehike/blocks"
2 | import { z } from "zod"
3 |
4 | export function Examples(props: unknown) {
5 | const { blocks, title, children } = parseProps(
6 | props,
7 | Block.extend({
8 | blocks: z.array(
9 | Block.extend({
10 | repo: ImageBlock,
11 | img: ImageBlock,
12 | }),
13 | ),
14 | }),
15 | )
16 | return (
17 |
18 | {title}
19 | {children}
20 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/apps/web/content/docs/code/fold.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Fold
3 | description: Fold annotation
4 | layout: PreviewAndImplementation
5 | ---
6 |
7 | ## !demo
8 |
9 | Fold inline content.
10 |
11 | Click on the ... to unfold the className
12 |
13 | ## !implementation
14 |
15 | ```tsx fold.tsx -c
16 | "use client"
17 | import { AnnotationHandler } from "codehike/code"
18 | import { useState } from "react"
19 |
20 | export const InlineFold: AnnotationHandler["Inline"] = ({ children }) => {
21 | const [folded, setFolded] = useState(true)
22 | if (!folded) {
23 | return children
24 | }
25 | return (
26 | setFolded(false)} aria-label="Expand">
27 | ...
28 |
29 | )
30 | }
31 | ```
32 |
33 | And then add the handler to your `Code` component:
34 |
35 | ```tsx code.tsx -c
36 | import { RawCode, Pre, highlight, AnnotationHandler } from "codehike/code"
37 | import { InlineFold } from "./fold"
38 |
39 | async function Code({ codeblock }: { codeblock: RawCode }) {
40 | const highlighted = await highlight(codeblock, "github-dark")
41 | return
42 | }
43 |
44 | const fold: AnnotationHandler = {
45 | name: "fold",
46 | Inline: InlineFold,
47 | }
48 | ```
49 |
--------------------------------------------------------------------------------
/apps/web/components/time-ago.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | export function TimeAgo({ date }: { date: string }) {
4 | const time = new Date(date)
5 | return (
6 |
7 | {getTimeAgo(time)}
8 |
9 | )
10 | }
11 |
12 | const MINUTE = 60
13 | const HOUR = MINUTE * 60
14 | const DAY = HOUR * 24
15 | const WEEK = DAY * 7
16 | const MONTH = DAY * 30
17 | const YEAR = DAY * 365
18 |
19 | function getTimeAgo(date: Date) {
20 | const secondsAgo = Math.round((Date.now() - Number(date)) / 1000)
21 |
22 | if (secondsAgo < MINUTE) {
23 | return secondsAgo + ` second${secondsAgo !== 1 ? "s" : ""} ago`
24 | }
25 |
26 | let divisor
27 | let unit = ""
28 |
29 | if (secondsAgo < HOUR) {
30 | ;[divisor, unit] = [MINUTE, "minute"]
31 | } else if (secondsAgo < DAY) {
32 | ;[divisor, unit] = [HOUR, "hour"]
33 | } else if (secondsAgo < WEEK) {
34 | ;[divisor, unit] = [DAY, "day"]
35 | } else if (secondsAgo < MONTH) {
36 | ;[divisor, unit] = [WEEK, "week"]
37 | } else if (secondsAgo < YEAR) {
38 | ;[divisor, unit] = [MONTH, "month"]
39 | } else {
40 | ;[divisor, unit] = [YEAR, "year"]
41 | }
42 |
43 | const count = Math.floor(secondsAgo / divisor)
44 | return `${count} ${unit}${count > 1 ? "s" : ""} ago`
45 | }
46 |
--------------------------------------------------------------------------------
/apps/web/content/docs/examples.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Full Examples and Templates
3 | description: Code Hike examples and templates
4 | ---
5 |
6 | import { Examples } from "./examples.tsx"
7 |
8 |
9 |
10 | Examples with minimal content showing how to use Code Hike with different frameworks and libraries.
11 |
12 | ## !! Next.js + Fumadocs
13 |
14 | 
15 | 
16 |
17 | ## !! Docusaurus
18 |
19 | 
20 | 
21 |
22 | ## !! Remotion
23 |
24 | 
25 | 
26 |
27 | ## !! Nextra
28 |
29 | 
30 | 
31 |
32 |
33 |
34 |
35 |
36 | Using Code Hike to rebuild some inspiring websites.
37 |
38 | ## !! Shopify API Reference
39 |
40 | 
41 | 
42 |
43 | ## !! Swift UI Tutorials
44 |
45 | 
46 | 
47 |
48 |
49 |
--------------------------------------------------------------------------------
/apps/web/content/blog/remotion/00.jsx:
--------------------------------------------------------------------------------
1 | // !mark 2
2 | import Content from "./content.md"
3 |
4 | // !f
5 |
6 | import {
7 | parseRoot,
8 | Block,
9 | } from "code-hike/blocks"
10 | import { z } from "zod"
11 |
12 | const Schema = Block.extend({
13 | steps: z.array(Block),
14 | })
15 |
16 | // !mark(1:4) 4
17 | const { steps } = parseRoot(
18 | Content,
19 | Schema,
20 | )
21 |
22 | import {
23 | AbsoluteFill,
24 | Sequence,
25 | Composition,
26 | } from "remotion"
27 |
28 | const STEP_FRAMES = 60
29 | function Video({ steps }) {
30 | return (
31 |
34 | {/* !mark(1:10) 0 */}
35 | {steps.map((step, i) => (
36 |
42 | {step.children}
43 |
44 | ))}
45 |
46 | )
47 | }
48 |
49 | export function RemotionRoot() {
50 | return (
51 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/apps/web/demos/occurrences/code.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { HighlightedCode, Pre } from "codehike/code"
4 | import React from "react"
5 |
6 | export function CodeWithOccurrences({ code }: { code: HighlightedCode }) {
7 | const ref = React.useRef(null)
8 | React.useEffect(() => {
9 | const handler: EventListener = (e) => {
10 | const selected = document.getSelection()!.toString().trim()
11 | ref.current!.querySelectorAll("span:not(:has(*))").forEach((element) => {
12 | if (element.textContent === selected) {
13 | element.setAttribute("data-selected", "true")
14 | } else {
15 | element.removeAttribute("data-selected")
16 | }
17 | })
18 | }
19 | document.addEventListener("selectionchange", handler)
20 | return () => {
21 | document.removeEventListener("selectionchange", handler)
22 | }
23 | }, [])
24 |
25 | return (
26 |
31 | )
32 | }
33 |
34 | // const Token: TokenComponent = ({ value, lineNumber, ...props }) => {
35 | // return (
36 | //
40 | // {value}
41 | //
42 | // )
43 | // }
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "turbo build --filter=codehike...",
5 | "web": "turbo dev --filter=web --filter=codehike... ",
6 | "test": "turbo test",
7 | "clean": "turbo clean && rm -rf node_modules",
8 | "format": "prettier --write \"**/*.{ts,tsx,md,mdx}\"",
9 | "changeset": "changeset",
10 | "version-packages": "changeset version",
11 | "release": "changeset publish",
12 | "canary": "pkg-pr-new publish --json=canary.json --comment=off --compact './packages/codehike'"
13 | },
14 | "devDependencies": {
15 | "@changesets/read": "0.6.1",
16 | "@changesets/cli": "2.27.1",
17 | "@changesets/changelog-github": "0.5.0",
18 | "@octokit/action": "7.0.0",
19 | "@octokit/rest": "21.0.2",
20 | "@actions/exec": "1.1.1",
21 | "@actions/github": "6.0.0",
22 | "prettier": "^3.1.1",
23 | "turbo": "^1.11.2",
24 | "pkg-pr-new": "0.0.24",
25 | "human-id": "4.1.1",
26 | "unified": "11.0.5",
27 | "remark-parse": "11.0.0",
28 | "remark-stringify": "11.0.0",
29 | "mdast-util-to-string": "4.0.0"
30 | },
31 | "packageManager": "pnpm@9.7.1",
32 | "repository": "code-hike/codehike",
33 | "homepage": "https://codehike.org",
34 | "funding": {
35 | "type": "github",
36 | "url": "https://github.com/sponsors/code-hike"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/content/blog/remotion/02.jsx:
--------------------------------------------------------------------------------
1 | import Content from "./content.md"
2 |
3 | const Schema = Block.extend({
4 | steps: z.array(
5 | Block.extend({
6 | code: HighlightedCodeBlock,
7 | }),
8 | ),
9 | })
10 | const { steps } = parseRoot(
11 | Content,
12 | Schema,
13 | )
14 |
15 | const STEP_FRAMES = 60
16 | function Video({ steps }) {
17 | return (
18 |
24 | {steps.map((step, i) => (
25 |
31 | {/* !focus */}
32 | {/* !mark */}
33 |
34 |
35 | ))}
36 |
37 | )
38 | }
39 |
40 | // !focus(1:3)
41 | // !mark(1:3)
42 | function Code({ code }) {
43 | return
44 | }
45 |
46 | export function RemotionRoot() {
47 | return (
48 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/apps/web/components/blocks-to-context.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipProvider,
6 | TooltipTrigger,
7 | } from "@/components/ui/tooltip"
8 |
9 | import React from "react"
10 |
11 | const BlocksContext = React.createContext(null)
12 |
13 | export function BlocksToContext({
14 | children,
15 | ...rest
16 | }: {
17 | children: React.ReactNode
18 | rest: any
19 | }) {
20 | return (
21 | {children}
22 | )
23 | }
24 |
25 | export function useBlocksContext(name: string) {
26 | return React.useContext(BlocksContext)[name]
27 | }
28 |
29 | export function WithTooltip({
30 | children,
31 | name,
32 | }: {
33 | children: React.ReactNode
34 | name: string
35 | }) {
36 | const block = useBlocksContext(name)
37 | const className = block.isCode
38 | ? "p-0 [&>*]:my-0 border-none overflow-auto rounded-none"
39 | : ""
40 | return (
41 |
42 |
43 |
44 |
45 | {children}
46 |
47 |
48 | {block?.children}
49 |
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/apps/web/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const HoverCard = HoverCardPrimitive.Root
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ))
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent }
30 |
--------------------------------------------------------------------------------
/apps/web/content/docs/code/line-numbers.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Line Numbers
3 | description: Line numbers
4 | layout: PreviewAndImplementation
5 | ---
6 |
7 | ## !demo
8 |
9 | Display line numbers.
10 |
11 |
12 |
13 | ## !implementation
14 |
15 | Using a [custom line component](/docs/concepts/annotations#customizing-line-and-token-components):
16 |
17 | ```tsx line-numbers.tsx -c
18 | import { AnnotationHandler, InnerLine } from "codehike/code"
19 |
20 | // !fold[/className="(.*?)"/gm]
21 | export const lineNumbers: AnnotationHandler = {
22 | name: "line-numbers",
23 | Line: (props) => {
24 | const width = props.totalLines.toString().length + 1
25 | return (
26 |
27 |
31 | {props.lineNumber}
32 |
33 |
34 |
35 | )
36 | },
37 | }
38 | ```
39 |
40 | Pass it to the `handlers` prop of the `Pre` component:
41 |
42 | ```tsx code.tsx -c
43 | import { lineNumbers } from "./line-numbers"
44 |
45 | async function Code({ codeblock }: { codeblock: RawCode }) {
46 | const highlighted = await highlight(codeblock, "github-dark")
47 | return
48 | }
49 | ```
50 |
--------------------------------------------------------------------------------
/apps/web/components/annotations/mark.tsx:
--------------------------------------------------------------------------------
1 | import { AnnotationHandler, BlockAnnotation, InnerLine } from "codehike/code"
2 |
3 | export const mark: AnnotationHandler = {
4 | name: "mark",
5 | Line: ({ annotation, ...props }) => {
6 | const color = getColor(annotation)
7 | return (
8 |
16 |
17 |
18 | )
19 | },
20 | Inline: ({ annotation, children }) => {
21 | const color = getColor(annotation)
22 | return (
23 |
30 | {children}
31 |
32 | )
33 | },
34 | }
35 |
36 | function getColor(annotation?: { query?: string }) {
37 | const n = Number(annotation?.query || "2") % colors.length
38 | return colors[n] || annotation?.query
39 | }
40 |
41 | const colors = [
42 | "#22c55e",
43 | "#14b8a6",
44 | "#0ea5e9",
45 | "#8b5cf6",
46 | "#d946ef",
47 | "#ec4899",
48 | ]
49 |
--------------------------------------------------------------------------------
/apps/web/demos/tabs/page.tsx:
--------------------------------------------------------------------------------
1 | import { Pre, RawCode, highlight } from "codehike/code"
2 | import Content from "./content.mdx"
3 | import { Block, CodeBlock, parseProps } from "codehike/blocks"
4 | import { z } from "zod"
5 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
6 |
7 | export default async function Page() {
8 | return
9 | }
10 |
11 | const Schema = Block.extend({ tabs: z.array(CodeBlock) })
12 |
13 | async function CodeWithTabs(props: unknown) {
14 | const { tabs } = parseProps(props, Schema)
15 | return
16 | }
17 |
18 | export async function CodeTabs(props: { tabs: RawCode[] }) {
19 | const { tabs } = props
20 | const highlighted = await Promise.all(
21 | tabs.map((tab) => highlight(tab, "github-dark")),
22 | )
23 | return (
24 |
25 |
26 | {tabs.map((tab) => (
27 |
28 | {tab.meta}
29 |
30 | ))}
31 |
32 | {tabs.map((tab, i) => (
33 |
34 |
35 |
36 | ))}
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/apps/web/components/annotations/icons.tsx:
--------------------------------------------------------------------------------
1 | import { RawCode } from "codehike/code"
2 | import { themeIcons } from "seti-icons"
3 |
4 | export function CodeIcon({ title }: { title: string }) {
5 | let filename = title || ""
6 | if (filename.endsWith(".mdx")) {
7 | filename = filename.slice(0, -4)
8 | filename += ".md"
9 | } else if (filename.endsWith(".mjs")) {
10 | filename = filename.slice(0, -4)
11 | filename += ".js"
12 | }
13 |
14 | const { svg, color } = getLightIcon(filename)
15 | const __html = svg.replace(
16 | /svg/,
17 | `svg fill='${color}' height='28' style='margin: -8px'`,
18 | )
19 | return (
20 |
24 | )
25 | }
26 |
27 | const getDarkIcon = themeIcons({
28 | blue: "#519aba",
29 | grey: "#4d5a5e",
30 | "grey-light": "#6d8086",
31 | green: "#8dc149",
32 | orange: "#e37933",
33 | pink: "#f55385",
34 | purple: "#a074c4",
35 | red: "#cc3e44",
36 | white: "#d4d7d6",
37 | yellow: "#cbcb41",
38 | ignore: "#41535b",
39 | })
40 |
41 | const getLightIcon = themeIcons({
42 | blue: "#498ba7",
43 | grey: "#455155",
44 | "grey-light": "#627379",
45 | green: "#7fae42",
46 | orange: "#f05138",
47 | pink: "#dd4b78",
48 | purple: "#9068b0",
49 | red: "#b8383d",
50 | white: "#bfc2c1",
51 | yellow: "#b7b73b",
52 | ignore: "#3b4b52",
53 | })
54 |
--------------------------------------------------------------------------------
/apps/web/demos/slideshow/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Block,
3 | CodeBlock,
4 | parseRoot,
5 | } from "codehike/blocks"
6 | import Content from "./content.md"
7 | import { z } from "zod"
8 | import {
9 | Selection,
10 | SelectionProvider,
11 | } from "codehike/utils/selection"
12 | import { Pre, RawCode, highlight } from "codehike/code"
13 | import { Controls } from "./controls"
14 | import { tokenTransitions } from "@/components/annotations/token-transitions"
15 |
16 | const Schema = Block.extend({
17 | steps: z.array(Block.extend({ code: CodeBlock })),
18 | })
19 |
20 | export default function Page() {
21 | const { steps } = parseRoot(Content, Schema)
22 | return (
23 |
24 | (
26 |
27 | ))}
28 | />
29 |
30 |
31 | step.children)}
33 | />
34 |
35 |
36 | )
37 | }
38 |
39 | async function Code({ codeblock }: { codeblock: RawCode }) {
40 | const highlighted = await highlight(
41 | codeblock,
42 | "github-dark",
43 | )
44 | return (
45 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/apps/web/demos/slideshow/content.md:
--------------------------------------------------------------------------------
1 | ## !!steps
2 |
3 | This is the first step. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
4 |
5 | ```js !
6 | function lorem(ipsum) {
7 | const sit = ipsum == null ? 0 : 1
8 | dolor = sit - amet(dolor)
9 | return consectetur(ipsum)
10 | }
11 | ```
12 |
13 | ## !!steps
14 |
15 | The second step, lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
16 |
17 | ```js !
18 | function lorem(ipsum, dolor = 1) {
19 | const sit = ipsum == null ? 0 : 1
20 | dolor = sit - amet(dolor)
21 | return sit ? consectetur(ipsum) : []
22 | }
23 | ```
24 |
25 | ## !!steps
26 |
27 | And the third step, lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
28 |
29 | ```js !
30 | function lorem(ipsum, dolor = 1) {
31 | const sit = ipsum == null ? 0 : 1
32 | dolor = sit - amet(dolor)
33 | if (dolor) {
34 | dolor += 100
35 | }
36 | return sit ? consectetur(ipsum) : []
37 | }
38 | ```
39 |
--------------------------------------------------------------------------------
/apps/web/ui/dependency-terminal.tsx:
--------------------------------------------------------------------------------
1 | import { CopyButton } from "@/components/copy-button"
2 | import { TerminalSquare } from "lucide-react"
3 | import { TabsContent, TabsList, TabsToggle } from "./tabs-toggle"
4 | import { RawCode, Pre, highlight } from "codehike/code"
5 |
6 | async function Code({ codeblock }: { codeblock: RawCode }) {
7 | const info = await highlight(codeblock, "github-dark")
8 | return
9 | }
10 |
11 | export function DependencyTerminal({ codeblock }: { codeblock: RawCode }) {
12 | const options = ["npm install", "yarn add", "pnpm install"].map(
13 | (command) => ({
14 | name: command.split(" ")[0],
15 | content: (
16 |
23 | ),
24 | }),
25 | )
26 |
27 | return (
28 |
32 |
33 |
34 | Terminal
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/apps/web/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipArrow = TooltipPrimitive.Arrow
15 |
16 | const TooltipContent = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, sideOffset = 4, ...props }, ref) => (
20 |
29 | ))
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
31 |
32 | export {
33 | Tooltip,
34 | TooltipTrigger,
35 | TooltipContent,
36 | TooltipProvider,
37 | TooltipArrow,
38 | }
39 |
--------------------------------------------------------------------------------
/apps/web/components/code/code-with-notes.tsx:
--------------------------------------------------------------------------------
1 | import { highlight } from "codehike/code"
2 | import { Block, CodeBlock, ImageBlock, parseProps } from "codehike/blocks"
3 | import { z } from "zod"
4 | import theme from "@/theme.mjs"
5 | import { HighCode } from "../code"
6 |
7 | const ContentSchema = z.object({
8 | code: CodeBlock,
9 | notes: z
10 | .array(
11 | Block.extend({
12 | code: CodeBlock.optional(),
13 | }),
14 | )
15 | .optional(),
16 | })
17 |
18 | export async function CodeWithNotes(props: unknown) {
19 | const { code, notes = [] } = parseProps(props, ContentSchema)
20 | const highlighted = await highlight(code, theme)
21 |
22 | // find matches between annotations and notes
23 | // and add the note as data to the annotation
24 | highlighted.annotations = await Promise.all(
25 | highlighted.annotations.map(async (a) => {
26 | const note = notes.find((n) => a.query && n.title === a.query)
27 | if (!note) return a
28 |
29 | let children = note.children
30 | if (note.code) {
31 | const highlighted = await highlight(note.code, theme)
32 | children = (
33 |
34 | )
35 | }
36 |
37 | return {
38 | ...a,
39 | data: {
40 | ...a.data,
41 | children,
42 | },
43 | }
44 | }),
45 | )
46 |
47 | return
48 | }
49 |
--------------------------------------------------------------------------------
/apps/web/components/demo.tsx:
--------------------------------------------------------------------------------
1 | import fs from "fs"
2 | import path from "path"
3 | import { Code } from "./code"
4 | import { cn } from "../lib/utils"
5 |
6 | export async function Demo({
7 | name,
8 | children,
9 | className,
10 | content = "content.md",
11 | }: {
12 | name: string
13 | content?: string
14 | children: React.ReactNode
15 | className?: string
16 | }) {
17 | const value = await fs.promises.readFile(
18 | path.join(process.cwd(), "demos", name, content),
19 | "utf-8",
20 | )
21 |
22 | const usage = (
23 |
31 | )
32 |
33 | const { default: Page } = await import(`@/demos/${name}/page`)
34 |
35 | const preview = (
36 |
37 |
38 | {children && (
39 |
40 | {children}
41 |
42 | )}
43 |
44 | )
45 |
46 | return (
47 | *]:flex-1 [&>*]:min-w-72",
50 | className,
51 | )}
52 | >
53 |
{usage}
54 | {preview}
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/nested-blocks.8.parsed-jsx.jsx:
--------------------------------------------------------------------------------
1 | var block = {
2 | "_data": {
3 | "header": "",
4 | },
5 | "children":
6 |
7 | hello
8 |
9 |
10 | hey
11 |
12 | ,
13 | "one": {
14 | "_data": {
15 | "header": "# !one uno",
16 | },
17 | "children":
18 |
19 | 1
20 |
21 |
22 | ho
23 |
24 | ,
25 | "extra": {
26 | "_data": {
27 | "header": "## !extra",
28 | },
29 | "children":
30 | extra
31 |
,
32 | "title": "",
33 | },
34 | "foo": [
35 | {
36 | "_data": {
37 | "header": "## !!foo 111",
38 | },
39 | "bar": {
40 | "_data": {
41 | "header": "### !bar b",
42 | },
43 | "children":
44 | 1.1.1
45 |
,
46 | "title": "b",
47 | },
48 | "children":
49 |
50 | 1.1
51 |
52 |
53 | more
54 |
55 | ,
56 | "title": "111",
57 | },
58 | {
59 | "_data": {
60 | "header": "## !!foo 222",
61 | },
62 | "children":
63 | hey
64 |
,
65 | "title": "222",
66 | },
67 | ],
68 | "title": "uno",
69 | },
70 | "title": "",
71 | }
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/ignore-codeblock.7.compiled-jsx.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic @jsxImportSource react*/
2 | function _createMdxContent(props) {
3 | const _components = {
4 | code: "code",
5 | p: "p",
6 | pre: "pre",
7 | ...props.components,
8 | },
9 | { MyCode } = _components
10 | if (!MyCode) _missingMdxReference("MyCode", true)
11 | return (
12 | <>
13 | <_components.p>{"hey"}
14 | {"\n"}
15 | <_components.pre>
16 | <_components.code className="language-mermaid">
17 | {
18 | "graph TD;\r\n A-->B;\r\n A-->C;\r\n B-->D;\r\n C-->D;\n"
19 | }
20 |
21 |
22 | {"\n"}
23 |
30 | >
31 | )
32 | }
33 | export default function MDXContent(props = {}) {
34 | const { wrapper: MDXLayout } = props.components || {}
35 | return MDXLayout ? (
36 |
37 | <_createMdxContent {...props} />
38 |
39 | ) : (
40 | _createMdxContent(props)
41 | )
42 | }
43 | function _missingMdxReference(id, component) {
44 | throw new Error(
45 | "Expected " +
46 | (component ? "component" : "object") +
47 | " `" +
48 | id +
49 | "` to be defined: you likely forgot to import, pass, or provide it.",
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/.github/scripts/git-utils.mjs:
--------------------------------------------------------------------------------
1 | import { exec, getExecOutput } from "@actions/exec"
2 | import github from "@actions/github"
3 | import fs from "fs"
4 |
5 | export async function checkout(branch) {
6 | let { stderr } = await getExecOutput("git", ["checkout", branch], {
7 | ignoreReturnCode: true,
8 | })
9 | let isCreatingBranch = !stderr
10 | .toString()
11 | .includes(`Switched to a new branch '${branch}'`)
12 | if (isCreatingBranch) {
13 | await exec("git", ["checkout", "-b", branch])
14 | }
15 | }
16 |
17 | export async function resetBranch() {
18 | // reset current branch to the commit that triggered the workflow
19 | await exec("git", ["reset", `--hard`, github.context.sha])
20 | }
21 |
22 | export async function commitAll(message) {
23 | await exec("git", ["add", "."])
24 | await exec("git", ["commit", "-m", message])
25 | }
26 |
27 | export async function forcePush(branch) {
28 | await exec("git", ["push", "origin", `HEAD:${branch}`, "--force"])
29 | }
30 |
31 | export async function setupUser() {
32 | await exec("git", ["config", "user.name", `"github-actions[bot]"`])
33 | await exec("git", [
34 | "config",
35 | "user.email",
36 | `"github-actions[bot]@users.noreply.github.com"`,
37 | ])
38 | await fs.promises.writeFile(
39 | `${process.env.HOME}/.netrc`,
40 | `machine github.com\nlogin github-actions[bot]\npassword ${process.env.GITHUB_TOKEN}`,
41 | )
42 | }
43 |
44 | export async function pushTags() {
45 | await exec("git", ["push", "origin", "--tags"])
46 | }
47 |
--------------------------------------------------------------------------------
/apps/web/app/landing/sponsors/update.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs")
2 | const path = require("path")
3 |
4 | const getOpenCollectiveSponsors = require("./oc-sponsors.js")
5 | const fetchGitHubSponsors = require("./gh-sponsors.js")
6 |
7 | const ignore = ["outerbounds", "github-sponsors", "guest-fbd7c737"]
8 | const replace = {
9 | speakeasybot: "speakeasy-api",
10 | ndimares: "speakeasy-api",
11 | hamelsmu: "outerbounds",
12 | "severin-ibarluzea": "seveibar",
13 | fbopensource: "facebook",
14 | }
15 | const others = [
16 | // from paypal
17 | { name: "matthiaszepper", amount: Math.round(94.3 + 108.71 + 47) },
18 | // missing from gh api
19 | { name: "github", amount: 20550 },
20 | ]
21 |
22 | Promise.all([getOpenCollectiveSponsors(), fetchGitHubSponsors()]).then(
23 | ([ocSponsors, ghSponsors]) => {
24 | const all = [...ocSponsors, ...ghSponsors, ...others]
25 | .filter((s) => !ignore.includes(s.name))
26 | .map((s) => ({ name: replace[s.name] || s.name, amount: s.amount }))
27 |
28 | const sponsors = []
29 | all.forEach((s) => {
30 | const index = sponsors.findIndex((sp) => sp.name === s.name)
31 | if (index === -1) {
32 | sponsors.push(s)
33 | } else {
34 | sponsors[index].amount += s.amount
35 | }
36 | })
37 |
38 | sponsors.sort((a, b) => b.amount - a.amount)
39 |
40 | console.table(sponsors)
41 | fs.writeFileSync(
42 | path.join(__dirname, "../sponsors.json"),
43 | JSON.stringify(sponsors, null, 2),
44 | )
45 | },
46 | )
47 |
--------------------------------------------------------------------------------
/apps/web/demos/hover/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AnnotationHandler,
3 | InnerLine,
4 | Pre,
5 | RawCode,
6 | highlight,
7 | } from "codehike/code"
8 | import Content from "./content.mdx"
9 | import "./styles.css"
10 |
11 | import React from "react"
12 |
13 | export default function Page() {
14 | return
15 | }
16 |
17 | function HoverContainer(props: { children: React.ReactNode }) {
18 | return (
19 |
20 | {props.children}
21 |
22 | )
23 | }
24 |
25 | function Link(props: { href?: string; children?: React.ReactNode }) {
26 | if (props.href?.startsWith("hover:")) {
27 | const hover = props.href.slice("hover:".length)
28 | return (
29 |
33 | {props.children}
34 |
35 | )
36 | } else {
37 | return
38 | }
39 | }
40 |
41 | async function Code({ codeblock }: { codeblock: RawCode }) {
42 | const highlighted = await highlight(codeblock, "github-dark")
43 | return
44 | }
45 |
46 | const hover: AnnotationHandler = {
47 | name: "hover",
48 | onlyIfAnnotated: true,
49 | Line: ({ annotation, ...props }) => (
50 |
55 | ),
56 | }
57 |
--------------------------------------------------------------------------------
/apps/web/demos/focus/focus.client.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React, { useLayoutEffect, useRef } from "react"
4 | import { AnnotationHandler, InnerPre, getPreRef } from "codehike/code"
5 |
6 | export const PreWithFocus: AnnotationHandler["PreWithRef"] = (props) => {
7 | const ref = getPreRef(props)
8 | useScrollToFocus(ref)
9 | return
10 | }
11 |
12 | function useScrollToFocus(ref: React.RefObject) {
13 | const firstRender = useRef(true)
14 | useLayoutEffect(() => {
15 | if (ref.current) {
16 | // find all descendants whith data-focus="true"
17 | const focusedElements = ref.current.querySelectorAll(
18 | "[data-focus=true]",
19 | ) as NodeListOf
20 |
21 | // find top and bottom of the focused elements
22 | const containerRect = ref.current.getBoundingClientRect()
23 | let top = Infinity
24 | let bottom = -Infinity
25 | focusedElements.forEach((el) => {
26 | const rect = el.getBoundingClientRect()
27 | top = Math.min(top, rect.top - containerRect.top)
28 | bottom = Math.max(bottom, rect.bottom - containerRect.top)
29 | })
30 |
31 | // scroll to the focused elements if any part of them is not visible
32 | if (bottom > containerRect.height || top < 0) {
33 | ref.current.scrollTo({
34 | top: ref.current.scrollTop + top - 10,
35 | behavior: firstRender.current ? "instant" : "smooth",
36 | })
37 | }
38 | firstRender.current = false
39 | }
40 | })
41 | }
42 |
--------------------------------------------------------------------------------
/apps/web/components/annotations/focus.client.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { CustomPreProps, InnerPre, getPreRef } from "codehike/code"
4 | import { useLayoutEffect, useRef } from "react"
5 |
6 | export function PreWithRef(props: CustomPreProps) {
7 | const ref = getPreRef(props)
8 | useScrollToFocus(ref)
9 | return
10 | }
11 |
12 | function useScrollToFocus(ref: React.RefObject) {
13 | const firstRender = useRef(true)
14 | useLayoutEffect(() => {
15 | if (ref.current) {
16 | // find all descendants whith data-focus="true"
17 | const focusedElements = ref.current.querySelectorAll(
18 | "[data-focus=true]",
19 | ) as NodeListOf
20 |
21 | // container rect
22 | const containerRect = ref.current.getBoundingClientRect()
23 |
24 | // find top and bottom of the focused elements
25 | let top = Infinity
26 | let bottom = -Infinity
27 | focusedElements.forEach((el) => {
28 | const rect = el.getBoundingClientRect()
29 | top = Math.min(top, rect.top - containerRect.top)
30 | bottom = Math.max(bottom, rect.bottom - containerRect.top)
31 | })
32 |
33 | // scroll to the focused elements if any part of them is not visible
34 | if (bottom > containerRect.height || top < 0) {
35 | ref.current.scrollTo({
36 | top: ref.current.scrollTop + top - 10,
37 | behavior: firstRender.current ? "instant" : "smooth",
38 | })
39 | }
40 | firstRender.current = false
41 | }
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/.github/scripts/pr-merged.mjs:
--------------------------------------------------------------------------------
1 | import { Octokit } from "@octokit/action"
2 | import { IDENTIFIER, PACKAGE_NAME } from "./params.mjs"
3 | import github from "@actions/github"
4 |
5 | const octokit = new Octokit({})
6 | const prNumber = github.context.payload.pull_request.number
7 |
8 | console.log("Querying closing issues")
9 | const query = `query ($owner: String!, $repo: String!, $prNumber: Int!) {
10 | repository(owner: $owner, name: $repo) {
11 | pullRequest(number: $prNumber) {
12 | title
13 | state
14 | closingIssuesReferences(first: 10) {
15 | nodes {
16 | number
17 | }
18 | }
19 | }
20 | }
21 | }`
22 | const result = await octokit.graphql(query, {
23 | ...github.context.repo,
24 | prNumber: Number(prNumber),
25 | })
26 |
27 | const body = `${IDENTIFIER}
28 | This issue has been fixed but not yet released.
29 |
30 | Try it in your project before the release with:
31 |
32 | ${"```"}
33 | npm i https://pkg.pr.new/${PACKAGE_NAME}@${prNumber}
34 | ${"```"}
35 |
36 | Or wait for the next [release](https://github.com/${github.context.repo.owner}/${github.context.repo.repo}/pulls?q=is%3Aopen+is%3Apr+label%3Arelease).
37 | `
38 |
39 | console.log("Commenting issues")
40 | await Promise.all(
41 | result.repository.pullRequest.closingIssuesReferences.nodes.map(
42 | async ({ number }) => {
43 | console.log("Commenting issue", number)
44 | await octokit.issues.createComment({
45 | ...github.context.repo,
46 | issue_number: number,
47 | body,
48 | })
49 | },
50 | ),
51 | )
52 |
--------------------------------------------------------------------------------
/apps/web/content/blog/remotion/03.jsx:
--------------------------------------------------------------------------------
1 | import Content from "./content.md"
2 |
3 | const Schema = Block.extend({
4 | steps: z.array(
5 | Block.extend({
6 | code: HighlightedCodeBlock,
7 | }),
8 | ),
9 | })
10 | const { steps } = parseRoot(
11 | Content,
12 | Schema,
13 | )
14 |
15 | const STEP_FRAMES = 60
16 | function Video({ steps }) {
17 | return (
18 |
24 | {steps.map((step, i) => (
25 |
31 |
32 |
33 | ))}
34 |
35 | )
36 | }
37 |
38 | // !focus(1:17)
39 | // !mark(1:17)
40 | function Code({ code }) {
41 | return (
42 |
43 | )
44 | }
45 |
46 | const mark = {
47 | name: "mark",
48 | Block: ({ children }) => {
49 | const background = "#F2CC6044"
50 | return (
51 |
52 | {children}
53 |
54 | )
55 | },
56 | }
57 |
58 | export function RemotionRoot() {
59 | return (
60 |
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/apps/web/demos/mark/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | RawCode,
3 | Pre,
4 | highlight,
5 | AnnotationHandler,
6 | InnerLine,
7 | } from "codehike/code"
8 | import Content from "./content.md"
9 |
10 | export default function Page() {
11 | return
12 | }
13 |
14 | const mark: AnnotationHandler = {
15 | name: "mark",
16 | Line: ({ annotation, ...props }) => {
17 | const color = annotation?.query || "rgb(14 165 233)"
18 | return (
19 |
27 |
28 |
29 | )
30 | },
31 | Inline: ({ annotation, children }) => {
32 | const color = annotation?.query || "rgb(14 165 233)"
33 | return (
34 |
41 | {children}
42 |
43 | )
44 | },
45 | }
46 |
47 | async function Code({ codeblock }: { codeblock: RawCode }) {
48 | const highlighted = await highlight(codeblock, "github-dark")
49 | return (
50 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/apps/web/demos/language-switcher/multi-code.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { HighlightedCode, Pre, RawCode, highlight } from "codehike/code"
4 |
5 | import { useState } from "react"
6 | import {
7 | Select,
8 | SelectContent,
9 | SelectItem,
10 | SelectTrigger,
11 | SelectValue,
12 | } from "@/components/ui/select"
13 | import { tokenTransitions } from "@/components/annotations/token-transitions"
14 |
15 | export function Code({ highlighted }: { highlighted: HighlightedCode[] }) {
16 | const [selectedLang, setSelectedLang] = useState(highlighted[0].lang)
17 | const selectedCode = highlighted.find((code) => code.lang === selectedLang)!
18 |
19 | return (
20 |
21 |
26 |
27 |
32 |
33 |
34 |
35 |
36 | {highlighted.map(({ lang }, index) => (
37 |
38 | {lang}
39 |
40 | ))}
41 |
42 |
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/apps/web/content/docs/code/diff.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Diff
3 | description: Diff
4 | layout: PreviewAndImplementation
5 | ---
6 |
7 | ## !demo
8 |
9 | Show inserted and deleted lines.
10 |
11 |
12 |
13 | ## !implementation
14 |
15 | There are two parts:
16 |
17 | - for the `+` and `-` icons, we customize the `Line` and prepend the `annotation.query`
18 | - for the border and color we use the `transform` function to add `mark` annotations and use the `AnnotationHandler` from the [mark example](/docs/code/mark)
19 |
20 | ```tsx diff.tsx -c
21 | import { AnnotationHandler, InnerLine, BlockAnnotation } from "codehike/code"
22 | // !fold[/className="(.*?)"/gm]
23 |
24 | export const diff: AnnotationHandler = {
25 | name: "diff",
26 | onlyIfAnnotated: true,
27 | transform: (annotation: BlockAnnotation) => {
28 | const color = annotation.query == "-" ? "#f85149" : "#3fb950"
29 | return [annotation, { ...annotation, name: "mark", query: color }]
30 | },
31 | Line: ({ annotation, ...props }) => (
32 | <>
33 |
34 | {annotation?.query}
35 |
36 |
37 | >
38 | ),
39 | }
40 | ```
41 |
42 | Then pass the `mark` and `diff` handlers to the `Pre` component:
43 |
44 | ```tsx code.tsx -c
45 | import { diff } from "./diff"
46 | import { mark } from "./mark"
47 |
48 | async function Code({ codeblock }: { codeblock: RawCode }) {
49 | const highlighted = await highlight(codeblock, "github-dark")
50 | return
51 | }
52 | ```
53 |
--------------------------------------------------------------------------------
/apps/web/content/docs/code/word-wrap.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Word Wrap
3 | description: Word wrap
4 | layout: PreviewAndImplementation
5 | ---
6 |
7 | ## !demo
8 |
9 | Wrap lines to avoid horizontal overflow.
10 |
11 | Drag the right handle to resize the width
12 |
13 | ## !implementation
14 |
15 | The easy way to make the code wrap is to style the ` ` component with `white-space: pre-wrap;`. But, when wrapping code, it's better to wrap the lines at the same indentation level. To do this, we can adjust the `text-indent` of each line:
16 |
17 | ```tsx word-wrap.tsx -c
18 | import {
19 | AnnotationHandler,
20 | InnerLine,
21 | InnerPre,
22 | InnerToken,
23 | } from "codehike/code"
24 |
25 | export const wordWrap: AnnotationHandler = {
26 | name: "word-wrap",
27 | Pre: (props) => ,
28 | Line: (props) => (
29 |
30 |
36 | {props.children}
37 |
38 |
39 | ),
40 | Token: (props) => ,
41 | }
42 | ```
43 |
44 | Pass it to the `handlers` prop of the `Pre` component:
45 |
46 | ```tsx code.tsx -c
47 | import { wordWrap } from "./word-wrap"
48 |
49 | async function Code({ codeblock }: { codeblock: RawCode }) {
50 | const highlighted = await highlight(codeblock, "github-dark")
51 | return
52 | }
53 | ```
54 |
--------------------------------------------------------------------------------
/apps/web/content/docs/code/copy-button.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Copy Button
3 | description: Copy Button
4 | layout: PreviewAndImplementation
5 | ---
6 |
7 | ## !demo
8 |
9 | Add a copy button to a code block.
10 |
11 |
12 |
13 | ## !implementation
14 |
15 | First we make the button component:
16 |
17 | ```tsx button.tsx -c
18 | "use client"
19 |
20 | import { Copy, Check } from "lucide-react"
21 | import { useState } from "react"
22 |
23 | // !fold[/className="(.*?)"/gm]
24 | export function CopyButton({ text }: { text: string }) {
25 | const [copied, setCopied] = useState(false)
26 |
27 | return (
28 | {
32 | navigator.clipboard.writeText(text)
33 | setCopied(true)
34 | setTimeout(() => setCopied(false), 1200)
35 | }}
36 | >
37 | {copied ? : }
38 |
39 | )
40 | }
41 | ```
42 |
43 | And then we use it when rendering the code block:
44 |
45 | ```tsx code.tsx -c
46 | import { Pre, RawCode, highlight } from "codehike/code"
47 | import { CopyButton } from "./button"
48 |
49 | // !fold[/className="(.*?)"/gm]
50 | async function Code({ codeblock }: { codeblock: RawCode }) {
51 | const highlighted = await highlight(codeblock, "github-dark")
52 |
53 | return (
54 |
58 | )
59 | }
60 | ```
61 |
--------------------------------------------------------------------------------
/apps/web/components/annotations/token-transitions.client.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { CustomPreProps, InnerPre, getPreRef } from "codehike/code"
4 | import {
5 | TokenTransitionsSnapshot,
6 | calculateTransitions,
7 | getStartingSnapshot,
8 | } from "codehike/utils/token-transitions"
9 | import React from "react"
10 |
11 | const MAX_TRANSITION_DURATION = 900 // milliseconds
12 | export class PreWithRef extends React.Component {
13 | ref: React.RefObject
14 | constructor(props: CustomPreProps) {
15 | super(props)
16 | this.ref = getPreRef(this.props)
17 | }
18 |
19 | render() {
20 | return
21 | }
22 |
23 | getSnapshotBeforeUpdate() {
24 | return getStartingSnapshot(this.ref.current!)
25 | }
26 |
27 | componentDidUpdate(
28 | prevProps: never,
29 | prevState: never,
30 | snapshot: TokenTransitionsSnapshot,
31 | ) {
32 | const transitions = calculateTransitions(this.ref.current!, snapshot)
33 | transitions.forEach(({ element, keyframes, options }) => {
34 | const { translateX, translateY, ...kf } = keyframes as any
35 | if (translateX && translateY) {
36 | kf.translate = [
37 | `${translateX[0]}px ${translateY[0]}px`,
38 | `${translateX[1]}px ${translateY[1]}px`,
39 | ]
40 | }
41 | element.animate(kf, {
42 | duration: options.duration * MAX_TRANSITION_DURATION,
43 | delay: options.delay * MAX_TRANSITION_DURATION,
44 | easing: options.easing,
45 | fill: "both",
46 | })
47 | })
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/packages/codehike/tests/md-suite/static-children.7.compiled-js.js:
--------------------------------------------------------------------------------
1 | import {
2 | Fragment as _Fragment,
3 | jsx as _jsx,
4 | jsxs as _jsxs,
5 | } from "react/jsx-runtime"
6 | function _createMdxContent(props) {
7 | const _components = {
8 | p: "p",
9 | slot: "slot",
10 | ...props.components,
11 | }
12 | return _jsxs(_Fragment, {
13 | children: [
14 | _jsx("div", {
15 | children: _jsxs(_Fragment, {
16 | children: [
17 | _jsx(_components.p, {
18 | children: "hey",
19 | }),
20 | _jsx(_components.p, {
21 | children: "ho",
22 | }),
23 | ],
24 | }),
25 | title: "",
26 | _data: {
27 | header: "",
28 | },
29 | foo: {
30 | children: _jsx(_components.p, {
31 | children: "bar",
32 | }),
33 | title: "",
34 | _data: {
35 | header: "## !foo",
36 | },
37 | },
38 | }),
39 | "\n",
40 | _jsxs("section", {
41 | children: [
42 | _jsx(_components.p, {
43 | children: "asdf",
44 | }),
45 | _jsx(_components.p, {
46 | children: "aa",
47 | }),
48 | ],
49 | }),
50 | ],
51 | })
52 | }
53 | export default function MDXContent(props = {}) {
54 | const { wrapper: MDXLayout } = props.components || {}
55 | return MDXLayout
56 | ? _jsx(MDXLayout, {
57 | ...props,
58 | children: _jsx(_createMdxContent, {
59 | ...props,
60 | }),
61 | })
62 | : _createMdxContent(props)
63 | }
64 |
--------------------------------------------------------------------------------
/apps/web/content/components/slideshow/slides.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import * as React from "react"
3 |
4 | const StepIndexContext = React.createContext<
5 | [number, React.Dispatch>]
6 | >([0, () => {}])
7 |
8 | export function Slides({
9 | slides,
10 | }: {
11 | slides: React.ReactNode[]
12 | }) {
13 | const [selectedIndex, setSelectedIndex] =
14 | React.useState(0)
15 |
16 | return (
17 |
20 | {slides[selectedIndex]}
21 |
22 | )
23 | }
24 |
25 | export function Controls({ length }: { length: number }) {
26 | const [selectedIndex, setSelectedIndex] =
27 | React.useContext(StepIndexContext)
28 |
29 | return (
30 |
31 |
34 | setSelectedIndex(Math.max(0, selectedIndex - 1))
35 | }
36 | >
37 | Prev
38 |
39 | {[...Array(length)].map((_, i) => (
40 | setSelectedIndex(i)}
46 | />
47 | ))}
48 |
51 | setSelectedIndex(
52 | Math.min(length - 1, selectedIndex + 1),
53 | )
54 | }
55 | >
56 | Next
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/apps/web/ui/tabs-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { ToggleGroup, ToggleGroupItem } from "./toggle-group"
3 |
4 | import React from "react"
5 |
6 | const TabsContext = React.createContext({
7 | selectedIndex: 0,
8 | setSelectedIndex: () => {},
9 | options: [],
10 | })
11 |
12 | export function TabsToggle({ children, className, options }: any) {
13 | const [selectedIndex, setSelectedIndex] = React.useState(0)
14 | return (
15 |
16 | {children}
17 |
18 | )
19 | }
20 |
21 | export function TabsList({ className }: any) {
22 | const { selectedIndex, setSelectedIndex, options } =
23 | React.useContext(TabsContext)
24 | return (
25 | {
30 | console.log(value)
31 | setSelectedIndex(Number(value))
32 | }}
33 | value={String(selectedIndex)}
34 | >
35 | {options.map((option: any, i: number) => (
36 |
42 | {option.name}
43 |
44 | ))}
45 |
46 | )
47 | }
48 |
49 | export function TabsContent({ className }: any) {
50 | const { selectedIndex, options } = React.useContext(TabsContext)
51 | const option = options[selectedIndex]
52 | return {option.content}
53 | }
54 |
--------------------------------------------------------------------------------
/apps/web/app/test/content.md:
--------------------------------------------------------------------------------
1 | ## The mother of all codeblocks
2 |
3 | ```js foo.js -awnc
4 | // !collapse(1:4)
5 | function lorem(ipsum, dolor = 1) {
6 | const sit = ipsum == null ? 0 : ipsum.sit
7 | // !mark(1:2)
8 | dolor = sit - amet(dolor)
9 | // !callout[/sit/] lorem ipsum dolor sit
10 | return sit ? consectetur(ipsum) : []
11 | }
12 |
13 | // !collapse(1:4) collapsed
14 | function ipsum(ipsum, dolor = 1) {
15 | const sit = ipsum == null ? 0 : ipsum.sit
16 | // !mark(1:2)
17 | dolor = sit - amet(dolor)
18 | return sit ? consectetur(ipsum) : []
19 | }
20 | ```
21 |
22 | ```js foo.js -anc
23 | // !collapse(1:4)
24 | function lorem(ipsum, dolor = 1) {
25 | const sit = ipsum == null ? 0 : ipsum.sit
26 | // !mark(1:2)
27 | dolor = sit - amet(dolor)
28 | // !callout[/sit/] lorem ipsum dolor sit
29 | return sit ? consectetur(ipsum) : []
30 | }
31 |
32 | // !collapse(1:4) collapsed
33 | function ipsum(ipsum, dolor = 1) {
34 | const sit = ipsum == null ? 0 : ipsum.sit
35 | // !mark(1:2)
36 | dolor = sit - amet(dolor)
37 | return sit ? consectetur(ipsum) : []
38 | }
39 | ```
40 |
41 | ```js -c
42 | function lorem(ipsum, dolor = 1) {
43 | const sit = ipsum == null ? 0 : ipsum.sit
44 | // !mark(1:2)
45 | dolor = sit - amet(dolor)
46 | // !callout[/sit/] lorem ipsum dolor sit
47 | return sit ? consectetur(ipsum) : []
48 | }
49 |
50 | function ipsum(ipsum, dolor = 1) {
51 | const sit = ipsum == null ? 0 : ipsum.sit
52 | // !mark(1:2)
53 | dolor = sit - amet(dolor)
54 | return sit ? consectetur(ipsum) : []
55 | }
56 | ```
57 |
58 | ```bash -c
59 | npx create-next-app -e https://github.com/code-hike/v1-starter
60 | ```
61 |
--------------------------------------------------------------------------------
/.github/scripts/md-utils.mjs:
--------------------------------------------------------------------------------
1 | import { unified } from "unified"
2 | import remarkParse from "remark-parse"
3 | import remarkStringify from "remark-stringify"
4 | import * as mdastToString from "mdast-util-to-string"
5 |
6 | const BumpLevels = {
7 | dep: 0,
8 | patch: 1,
9 | minor: 2,
10 | major: 3,
11 | }
12 |
13 | export function getChangelogEntry(changelog, version) {
14 | let ast = unified().use(remarkParse).parse(changelog)
15 |
16 | let highestLevel = BumpLevels.dep
17 |
18 | let nodes = ast.children
19 | let headingStartInfo
20 | let endIndex
21 |
22 | for (let i = 0; i < nodes.length; i++) {
23 | let node = nodes[i]
24 | if (node.type === "heading") {
25 | let stringified = mdastToString.toString(node)
26 | let match = stringified.toLowerCase().match(/(major|minor|patch)/)
27 | if (match !== null) {
28 | let level = BumpLevels[match[0]]
29 | highestLevel = Math.max(level, highestLevel)
30 | }
31 | if (headingStartInfo === undefined && stringified === version) {
32 | headingStartInfo = {
33 | index: i,
34 | depth: node.depth,
35 | }
36 | continue
37 | }
38 | if (
39 | endIndex === undefined &&
40 | headingStartInfo !== undefined &&
41 | headingStartInfo.depth === node.depth
42 | ) {
43 | endIndex = i
44 | break
45 | }
46 | }
47 | }
48 | if (headingStartInfo) {
49 | ast.children = ast.children.slice(headingStartInfo.index + 1, endIndex)
50 | }
51 | return {
52 | content: unified().use(remarkStringify).stringify(ast),
53 | highestLevel: highestLevel,
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/apps/web/components/annotations/callout.tsx:
--------------------------------------------------------------------------------
1 | import { AnnotationHandler, InlineAnnotation, InnerLine } from "codehike/code"
2 |
3 | export const callout: AnnotationHandler = {
4 | name: "callout",
5 | transform: (annotation: InlineAnnotation) => {
6 | // transform inline annotation to block annotation
7 | const { name, query, lineNumber, fromColumn, toColumn } = annotation
8 | return {
9 | name,
10 | query,
11 | fromLineNumber: lineNumber,
12 | toLineNumber: lineNumber,
13 | data: {
14 | ...annotation.data,
15 | column: (fromColumn + toColumn) / 2,
16 | },
17 | }
18 | },
19 | AnnotatedLine: ({ annotation, ...props }) => {
20 | const { column } = annotation.data
21 | const { indentation, children } = props
22 | return (
23 |
24 | {children}
25 |
32 |
36 | {annotation.data.children || (
37 |
{annotation.query}
38 | )}
39 |
40 |
41 | )
42 | },
43 | }
44 |
--------------------------------------------------------------------------------
/apps/web/demos/callout/page.tsx:
--------------------------------------------------------------------------------
1 | import Content from "./content.md"
2 | import { RawCode, Pre, highlight } from "codehike/code"
3 | import { InlineAnnotation, AnnotationHandler } from "codehike/code"
4 |
5 | export default function Page() {
6 | return
7 | }
8 |
9 | export async function Code({ codeblock }: { codeblock: RawCode }) {
10 | const highlighted = await highlight(codeblock, "github-dark")
11 |
12 | return (
13 |
18 | )
19 | }
20 |
21 | const callout: AnnotationHandler = {
22 | name: "callout",
23 | transform: (annotation: InlineAnnotation) => {
24 | const { name, query, lineNumber, fromColumn, toColumn, data } = annotation
25 | return {
26 | name,
27 | query,
28 | fromLineNumber: lineNumber,
29 | toLineNumber: lineNumber,
30 | data: { ...data, column: (fromColumn + toColumn) / 2 },
31 | }
32 | },
33 | Block: ({ annotation, children }) => {
34 | const { column } = annotation.data
35 | return (
36 | <>
37 | {children}
38 |
42 |
46 | {annotation.query}
47 |
48 | >
49 | )
50 | },
51 | }
52 |
--------------------------------------------------------------------------------
/packages/codehike/src/mdx/1.0.transform-hikes.ts:
--------------------------------------------------------------------------------
1 | import { Root } from "mdast"
2 | import { MdxJsxFlowElement } from "mdast-util-mdx-jsx"
3 | import { visit } from "unist-util-visit"
4 | import { isHikeElement, listToSection } from "./1.1.remark-list-to-section.js"
5 | import { sectionToAttribute } from "./1.2.remark-section-to-attribute.js"
6 | import { CodeHikeConfig } from "./config.js"
7 |
8 | export async function transformAllHikes(root: Root, config: CodeHikeConfig) {
9 | let tree = wrapInHike(root)
10 |
11 | const hikes: MdxJsxFlowElement[] = []
12 |
13 | visit(tree, "mdxJsxFlowElement", (node) => {
14 | if (node.children?.some(isHikeElement)) {
15 | hikes.push(node)
16 | }
17 | })
18 |
19 | await Promise.all(hikes.map((h) => transformRemarkHike(h, config)))
20 |
21 | return tree
22 | }
23 |
24 | function wrapInHike(root: Root) {
25 | // if we find any hikeable element outside of s,
26 | // let's wrap everything in a
27 | if (root.children?.some(isHikeElement)) {
28 | root.children = [
29 | {
30 | type: "mdxJsxFlowElement",
31 | name: "slot",
32 | attributes: [],
33 | // todo what is different between RootContent and (BlockContent | DefinitionContent)
34 | children: root.children as any,
35 | },
36 | ]
37 | }
38 | return root
39 | }
40 |
41 | async function transformRemarkHike(
42 | node: MdxJsxFlowElement,
43 | config: CodeHikeConfig,
44 | ) {
45 | const section = await listToSection(node, config)
46 | const { children, attributes } = sectionToAttribute(section)
47 |
48 | node.children = children
49 | node.attributes.push(...attributes)
50 |
51 | return node
52 | }
53 |
--------------------------------------------------------------------------------
/apps/web/app/demo/client.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | AnnotationHandler,
5 | CustomPreProps,
6 | InnerPre,
7 | getPreRef,
8 | } from "codehike/code"
9 | import { forwardRef } from "react"
10 | import React from "react"
11 | import {
12 | TokenTransitionsSnapshot,
13 | calculateTransitions,
14 | getStartingSnapshot,
15 | } from "codehike/utils/token-transitions"
16 |
17 | const MAX_TRANSITION_DURATION = 900 // milliseconds
18 | export class SmoothPre extends React.Component {
19 | ref: React.RefObject
20 | constructor(props: CustomPreProps) {
21 | super(props)
22 | this.ref = getPreRef(this.props)
23 | }
24 |
25 | render() {
26 | return
27 | }
28 |
29 | getSnapshotBeforeUpdate() {
30 | return getStartingSnapshot(this.ref.current!)
31 | }
32 |
33 | componentDidUpdate(
34 | prevProps: never,
35 | prevState: never,
36 | snapshot: TokenTransitionsSnapshot,
37 | ) {
38 | const transitions = calculateTransitions(this.ref.current!, snapshot)
39 | transitions.forEach(({ element, keyframes, options }) => {
40 | const { translateX, translateY, ...kf } = keyframes as any
41 | if (translateX && translateY) {
42 | kf.translate = [
43 | `${translateX[0]}px ${translateY[0]}px`,
44 | `${translateX[1]}px ${translateY[1]}px`,
45 | ]
46 | }
47 | element.animate(kf, {
48 | duration: options.duration * MAX_TRANSITION_DURATION,
49 | delay: options.delay * MAX_TRANSITION_DURATION,
50 | easing: options.easing,
51 | fill: "both",
52 | })
53 | })
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/apps/web/app/test/client.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | AnnotationHandler,
5 | CustomPreProps,
6 | InnerPre,
7 | getPreRef,
8 | } from "codehike/code"
9 | import { forwardRef } from "react"
10 | import React from "react"
11 | import {
12 | TokenTransitionsSnapshot,
13 | calculateTransitions,
14 | getStartingSnapshot,
15 | } from "codehike/utils/token-transitions"
16 |
17 | const MAX_TRANSITION_DURATION = 900 // milliseconds
18 | export class SmoothPre extends React.Component {
19 | ref: React.RefObject
20 | constructor(props: CustomPreProps) {
21 | super(props)
22 | this.ref = getPreRef(this.props)
23 | }
24 |
25 | render() {
26 | return
27 | }
28 |
29 | getSnapshotBeforeUpdate() {
30 | return getStartingSnapshot(this.ref.current!)
31 | }
32 |
33 | componentDidUpdate(
34 | prevProps: never,
35 | prevState: never,
36 | snapshot: TokenTransitionsSnapshot,
37 | ) {
38 | const transitions = calculateTransitions(this.ref.current!, snapshot)
39 | transitions.forEach(({ element, keyframes, options }) => {
40 | const { translateX, translateY, ...kf } = keyframes as any
41 | if (translateX && translateY) {
42 | kf.translate = [
43 | `${translateX[0]}px ${translateY[0]}px`,
44 | `${translateX[1]}px ${translateY[1]}px`,
45 | ]
46 | }
47 | element.animate(kf, {
48 | duration: options.duration * MAX_TRANSITION_DURATION,
49 | delay: options.delay * MAX_TRANSITION_DURATION,
50 | easing: options.easing,
51 | fill: "both",
52 | })
53 | })
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/apps/web/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./global.css"
2 | import { RootProvider } from "next-docs-ui/provider"
3 | import { Inter } from "next/font/google"
4 | import type { ReactNode } from "react"
5 | import { NavBar } from "../ui/nav"
6 | import { Analytics } from "@vercel/analytics/react"
7 |
8 | const inter = Inter({
9 | subsets: ["latin"],
10 | })
11 |
12 | import ch from "codehike/package.json"
13 | import { Metadata } from "next"
14 |
15 | export default function Layout({ children }: { children: ReactNode }) {
16 | return (
17 |
22 | {/* */}
23 |
24 |
25 |
26 | {children}
27 |
28 |
29 |
30 |
31 | )
32 | }
33 | export const metadata: Metadata = {
34 | title: "Code Hike",
35 | description:
36 | "Use Markdown and React to build rich content websites. Documentation, tutorials, blogs, videos, interactive walkthroughs, and more.",
37 | // metadataBase: new URL("https://codehike.org"),
38 | openGraph: {
39 | title: "Code Hike",
40 | images: `https://codehike.org/codehike.png`,
41 | siteName: "Code Hike",
42 | },
43 | twitter: {
44 | card: "summary_large_image",
45 | site: "@codehike_",
46 | creator: "@pomber",
47 | images: `https://codehike.org/codehike.png`,
48 | },
49 | alternates: {
50 | types: {
51 | "application/rss+xml": "https://codehike.org/blog/feed.xml",
52 | },
53 | },
54 | }
55 |
--------------------------------------------------------------------------------
/apps/web/demos/focus/code.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { HighlightedCode, Pre } from "codehike/code"
4 | import React, { useState } from "react"
5 | import { focus } from "./focus"
6 |
7 | const ranges = {
8 | lorem: { fromLineNumber: 1, toLineNumber: 5 },
9 | ipsum: { fromLineNumber: 7, toLineNumber: 11 },
10 | dolor: { fromLineNumber: 11, toLineNumber: 15 },
11 | }
12 |
13 | export function CodeContainer({ code }: { code: HighlightedCode }) {
14 | const [focused, setFocused] = useState<"lorem" | "ipsum" | "dolor">("dolor")
15 |
16 | return (
17 | <>
18 |
32 |
33 | You can also change the focus annotations on a rendered codeblock:
34 |
35 |
36 | setFocused("lorem")}
38 | disabled={focused === "lorem"}
39 | className="border border-current rounded px-2"
40 | >
41 | focus `lorem`
42 | {" "}
43 | setFocused("dolor")}
45 | disabled={focused === "dolor"}
46 | className="border border-current rounded px-2"
47 | >
48 | focus `dolor`
49 |
50 |
51 | >
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/apps/web/app/blog/og/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 | import { CodeHikeLogo } from "../../../ui/nav"
3 |
4 | const title = "The Curse of Markdown"
5 | const description = "And the content website wasteland"
6 | const date = "November 21, 2024"
7 |
8 | import pomber from "../[slug]/pomber.jpg"
9 |
10 | export default function Page() {
11 | return (
12 |
13 |
17 |
18 |
19 | Code Hike's blog
20 |
21 |
22 | {date}
23 |
24 |
25 |
{title}
26 |
27 | {description}
28 |
29 |
30 |
38 |
39 |
Rodrigo Pombo
40 |
@pomber
41 |
42 |
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/apps/web/content/docs/code/transpile.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Transpile
3 | description: Show code together with its transpiled version
4 | layout: PreviewAndImplementation
5 | ---
6 |
7 | ## !demo
8 |
9 | Sometimes you have a code block that you want to show together with its versions in different languages. It may be typescript and javascript, or sass and css, or maybe you want to transform a cURL command into calls to SDKs in different languages.
10 |
11 | If you are using React Server Components and you have a function to transpile the code, you can call that function inside the component.
12 |
13 | Here's an example showing how to transpile a typescript code block to javascript, and then showing both versions in tabs:
14 |
15 |
16 |
17 | ## !implementation
18 |
19 | ```tsx code.tsx -c
20 | import { RawCode } from "codehike/code"
21 | // !link[/tabs example/] tabs
22 | // CodeTabs is the component from the tabs example
23 | // !link[/CodeTabs/] tabs
24 | import { CodeTabs } from "@/components/tabs"
25 | import ts from "typescript"
26 |
27 | async function Code({ codeblock }: { codeblock: RawCode }) {
28 | // Since this is a RSC we can transpile stuff here
29 | // (there are probably more efficient ways to do this)
30 | const result = ts.transpileModule(codeblock.value, {
31 | compilerOptions: {
32 | module: ts.ModuleKind.CommonJS,
33 | target: ts.ScriptTarget.ESNext,
34 | },
35 | })
36 |
37 | const tsCode = {
38 | ...codeblock,
39 | meta: "typescript",
40 | }
41 | const jsCode = {
42 | ...codeblock,
43 | value: result.outputText,
44 | lang: "js",
45 | meta: "javascript",
46 | }
47 |
48 | return
49 | }
50 | ```
51 |
--------------------------------------------------------------------------------