Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[render] Render page title correctly #17

Merged
merged 1 commit into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions packages/render/src/components/page-title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ import { Icon } from "./icon"

export const PageTitle = ({ icon, children }: { icon: IconResponse; children: React.ReactNode }) => {
return (
<span className="notion-page-title">
<Icon icon={icon} width={20} className="notion-page-title-icon" />
<span className="notion-page-title-text">{children}</span>
// ref: .notion-page-title
<span className="inline-flex max-w-full items-center leading-[1.3] transition-[background] delay-[0s] duration-[120ms] ease-[ease-in]">
{/* ref: .notion-page-title-icon */}
{/* note: plus ref .notion-page-icon-inline for margins */}
<Icon icon={icon} width={20} className="me-1.5 ms-0.5 flex shrink-0 items-center justify-center rounded-[3px]" />
{/* ref: .notion-page-title-text */}
{/* note: Consider how to use the border-buttom here in combination with a link. See also `.notion-link .notion-page-title-text` */}
<span className="relative top-px overflow-hidden text-ellipsis whitespace-nowrap border-b border-solid border-b-[color:var(--fg-color-1)] font-medium leading-[1.3]">
{children}
</span>
</span>
)
}
141 changes: 65 additions & 76 deletions packages/render/src/components/text.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { CSSProperties } from "react"
import type { MentionRichTextItemResponse, RichTextItemResponse } from "@notionhq/client/build/src/api-endpoints"
import type {
MentionRichTextItemResponse,
RichTextItemResponse,
TextRichTextItemResponse,
} from "@notionhq/client/build/src/api-endpoints"

import { cn, notionColor, relativeNotionUrl } from "../util"
import { cn, idToUuid, notionColor, notionUrl } from "../util"
import { RenderOptions } from "../types"
import { A } from "./html/a"
import { PageTitle } from "./page-title"
Expand All @@ -27,97 +30,83 @@ const RichText = ({ value, options }: { value: RichTextItemResponse; options: Re
// Not supported
return <></>
}
let result = <>{mentionContent}</>
// FIXME(post-mvp): Merge annotations with "text".
if (value.annotations.color) {
result = <span style={notionColor(value.annotations.color)}>{result}</span>
}
if (value.annotations.bold) {
result = <b>{result}</b>
}
if (value.annotations.italic) {
result = <i>{result}</i>
}
if (value.annotations.strikethrough) {
result = <s>{result}</s>
}
if (value.annotations.underline) {
result = <span className="notion-inline-underscore">{result}</span>
}
if (value.annotations.code) {
result = <code className="notion-inline-code">{result}</code>
const mention = <FormattedText annotations={value.annotations}>{mentionContent}</FormattedText>
if (value.href === null) {
return mention
}

// Normal links are relative, e.g. /<uuid>, but mentions are absolute, e.g. https://www.notion.so/<uuid>
let mentionUrl = value.href !== null ? relativeNotionUrl(value.href) : null
if (mentionUrl) {
result = (
<ResolvedLink url={mentionUrl} options={options}>
{result}
</ResolvedLink>
// Normal links are relative, e.g. /<uuid>, but mentions are absolute, e.g. https://www.notion.so/<id>
const uuid = idToUuid(value.href.replace(`${notionUrl}/`, ""))
const resolvedLink = options.resolveLinkFn(uuid)
if (resolvedLink === null) {
// If the URL can't be resolved make it an external link.
return (
<options.htmlComponents.a href={value.href} target="_blank">
{mention}
</options.htmlComponents.a>
)
}
return result
case "text":
// ref: .notion-inline-underscore (+ b, i & s)
// whitespace-pre-wrap: Otherwise line breaks are not shown.
let text = (
<span
style={notionColor(value.annotations.color)}
className={cn(
"whitespace-pre-wrap",
value.annotations.bold ? "font-semibold" : "",
value.annotations.italic ? "italic" : "",
value.annotations.strikethrough ? "line-through" : "",
value.annotations.underline ? "underline" : "",
)}
>
{value.text.content}
</span>
return (
<options.htmlComponents.a href={resolvedLink.href}>
<PageTitle icon={resolvedLink.icon}>{mention}</PageTitle>
</options.htmlComponents.a>
)
if (value.annotations.code) {
// ref: .notion-inline-code
text = (
<code className="rounded-[3px] bg-[--bg-color-2] px-[0.4em] py-[0.2em] font-mono text-[85%] text-[#ff4081]">
{text}
</code>
)
}

const textUrl = value.text.link?.url ?? null
if (textUrl) {
case "text":
const text = <FormattedText annotations={value.annotations}>{value.text.content}</FormattedText>
const textUrl = value.text.link?.url
if (!textUrl) {
return text
}
// Relative Url
if (textUrl.startsWith("/")) {
const uuid = textUrl.substring(1) // remove the leading slash
const resolvedLink = options.resolveLinkFn(uuid)
return (
<ResolvedLink url={textUrl} options={options}>
{text}
</ResolvedLink>
<options.htmlComponents.a href={resolvedLink?.href ?? "#"}>
<PageTitle icon={resolvedLink?.icon ?? null}>{text}</PageTitle>
</options.htmlComponents.a>
)
}
return <>{text}</>
return (
<options.htmlComponents.a href={textUrl} target="_blank">
{text}
</options.htmlComponents.a>
)
}
}

const ResolvedLink = ({
url,
options,
const FormattedText = ({
annotations,
children,
}: {
url: string
options: RenderOptions
annotations: TextRichTextItemResponse["annotations"]
children: React.ReactNode
}) => {
const isRelative = url.startsWith("/")
if (!isRelative) {
return (
<options.htmlComponents.a href={url} target="_blank">
{children}
</options.htmlComponents.a>
)
// ref: .notion-inline-underscore (+ b, i & s)
// whitespace-pre-wrap: Otherwise line breaks are not shown.
const result = (
<span
style={notionColor(annotations.color)}
className={cn(
"whitespace-pre-wrap",
annotations.bold ? "font-semibold" : "",
annotations.italic ? "italic" : "",
annotations.strikethrough ? "line-through" : "",
annotations.underline ? "underline" : "",
)}
>
{children}
</span>
)
if (!annotations.code) {
return result
}
const resolvedLink = options.resolveLinkFn(url.substring(1)) // remove the leading
// ref: .notion-inline-code
return (
<options.htmlComponents.a href={resolvedLink?.href ?? "#"}>
<PageTitle icon={resolvedLink?.icon ?? null}>{children}</PageTitle>
</options.htmlComponents.a>
<code className="rounded-[3px] bg-[--bg-color-2] px-[0.4em] py-[0.2em] font-mono text-[85%] text-[#ff4081]">
{result}
</code>
)
}

Expand Down
9 changes: 5 additions & 4 deletions packages/render/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import { type CSSProperties } from "react"
// Just a helper type because it's not directly exposed.
type Color = ParagraphBlockObjectResponse["paragraph"]["color"]

export const notionUrl = "https://www.notion.so"

export const notionColor = (color: Color, backgroundCallout?: boolean): CSSProperties => {
if (color.endsWith("_background")) {
return { backgroundColor: `var(--notion-${color}${backgroundCallout ? "_co" : ""}` }
}
return { color: `var(--notion-${color})` }
}

export const relativeNotionUrl = (url: string) => {
return url.replace("https://www.notion.so", "")
}

// export const noopResolveLinkFn = (nId: string) => nId

export const cn = (...classes: string[]) => {
return classes.filter(Boolean).join(" ")
}

export const idToUuid = (id: string) =>
`${id.substring(0, 8)}-${id.substring(8, 12)}-${id.substring(12, 16)}-${id.substring(16, 20)}-${id.substring(20)}`