Files
rea/website/public/showcase/notion/index.html
T
12ee785e5e feat(website): follow the system dark mode and fix small-screen text (#1894)
* fix(website): keep text inside small screens and tidy small type and spacing

From a layout audit of the built pages:

- Long terminal prompts and code-header buttons wrap instead of being cut
  off at 320px (WCAG 1.4.10).
- Headings and table cells break long words, so the 1.4.12 text-spacing
  test no longer widens pages past 320px.
- Header and footer links get a 24px minimum height (2.5.8).
- Labels that rendered under 12px now render at 12px or more, and inline
  code never drops under 0.75rem.
- The home page's hero and definition rows share one column gap and align
  on the first baseline, and stacked boxes share one corner size.
- Highlight chips get room around the text, and the active run in a step
  listing is one band instead of a box per line.

Colors are unchanged.

🤖 Generated with Claude Code (Sonnet 5.5) 🦊

* feat(website): follow the system dark mode

The site was light only, so a reader with a dark system still got a white
page. Add one prefers-color-scheme: dark block to the stylesheet that swaps
the six color variables to Catppuccin Mocha (MIT) and overrides the fixed
colors that would break on a dark page: selection, buttons, tinted
highlights, flow nodes and arrows, and a visible border on the agent
terminal.

Add a dark theme-color tag beside the light one in the page template and
the 17 pages, and a Dark column to the style guide's color table.

Light colors are unchanged. The SVG figures follow in the next commit; the
blog page's inline strokes, the dinosaur game and the social card are left
as they are.

🤖 Generated with Claude Code (Sonnet 5.5) 🦊

* feat(website): give the five SVG figures a dark scheme

Pages load the figures with <img>, so page CSS cannot reach inside them.
Each figure now carries its own @media (prefers-color-scheme: dark) block
of attribute-selector overrides that map every light color to a Catppuccin
Mocha color, with one shared map across all five. No markup, labels,
shapes or positions change, and nothing uses !important.

Four figures also paint a mantle canvas so "Open figure" works on its own;
analyst-thinking.svg stays transparent, since it sits on the page rather
than in a figure box.

🤖 Generated with Claude Code (Sonnet 5.5) 🦊

* chore(website): refresh translation source references

The theme-color tags move the page lines that the catalogs cite, so
python3 scripts/website.py translations extract rewrites the source
references in messages.pot and the 18 PO files. No message text changes.
The same run moves the "Main navigation" entry to its new position in each
catalog.

🤖 Generated with Claude Code (Sonnet 5.5) 🦊

* fix(website): keep inline step highlights inline

The full-width band for the active step now applies only to code-step
blocks. The inline parameter highlights in the dx-ball and th04
signatures stay inline again.

🤖 Generated with Claude Code (Opus 5.5) 🦊

* test(website): check every page offers light and dark theme colors

Each authored reading page, and its built copy in every language,
must carry one theme-color tag for light and one for dark. The
stylesheet must keep its dark color scheme block. On the commit before
dark mode, this test fails on every page.

🤖 Generated with Claude Code (Opus 5.5) 🦊

* fix(website): draw the dinosaur game in the page colors

In dark mode the game box turned dark, but the game still drew with
its light colors, so the dinosaur, cacti and text fell to about 2:1
contrast. The canvas now reads five colors from CSS variables on
.dino-stage. Light values are the old ones, so light mode draws the
same pixels. Dark values are Catppuccin Mocha: 8.3:1 for the
dinosaur, 7.9:1 for cacti and text, 7.6:1 for the crash color.

The moved lines in dino-demo.js shift the translation catalog
references, so the catalogs are refreshed too (reference comments
only).

🤖 Generated with Claude Code (Opus 5.5) 🦊

* fix(website): recolor the Touhou flywheel arrows in dark mode

The inline flywheel SVG on the Touhou article hard-codes its stroke
colors, so on the dark page its connectors fell to about 2:1. Two
dark-only rules map those strokes to the page accent and muted colors
(7.8:1 and 7.4:1). Light mode is unchanged.

🤖 Generated with Claude Code (Opus 5.5) 🦊

* fix(website): redraw the dinosaur game when the system theme changes

The canvas reads its colors only when it draws, so an idle, paused or
crashed game kept the old palette after a theme switch. Each demo now
redraws on a prefers-color-scheme change.

The added lines shift the translation catalog references, so the
catalogs are refreshed too (reference comments only).

🤖 Generated with Claude Code (Opus 5.5) 🦊

---------

Co-authored-by: Claude Sonnet 5.5 <sonnet@cassie-pc>
Co-authored-by: Claude Opus 5.5 <opus@cassie-pc>
2026-10-12 03:17:49 +08:00

633 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Notion Electron Reverse Engineering: Clipboard — REA</title>
<meta
name="description"
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
/>
<!-- website:sharing:start -->
<!-- prettier-ignore -->
<link rel="canonical" href="https://rea.tools/showcase/notion/" />
<!-- website:languages:start -->
<!-- prettier-ignore -->
<link rel="alternate" hreflang="en" href="https://rea.tools/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="zh-CN" href="https://rea.tools/zh-CN/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="zh-TW" href="https://rea.tools/zh-TW/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="ja" href="https://rea.tools/ja/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="ko" href="https://rea.tools/ko/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="tr" href="https://rea.tools/tr/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="ru" href="https://rea.tools/ru/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="vi" href="https://rea.tools/vi/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="th" href="https://rea.tools/th/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="de" href="https://rea.tools/de/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="es" href="https://rea.tools/es/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="fr" href="https://rea.tools/fr/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="uk" href="https://rea.tools/uk/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="pl" href="https://rea.tools/pl/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="pt-BR" href="https://rea.tools/pt-BR/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="ar" href="https://rea.tools/ar/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="fa" href="https://rea.tools/fa/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="id" href="https://rea.tools/id/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="it" href="https://rea.tools/it/showcase/notion/" />
<!-- prettier-ignore -->
<link rel="alternate" hreflang="x-default" href="https://rea.tools/showcase/notion/" />
<!-- website:languages:end -->
<!-- prettier-ignore -->
<meta property="og:type" content="website" />
<!-- prettier-ignore -->
<meta property="og:site_name" content="REA" />
<!-- prettier-ignore -->
<meta property="og:title" content="Notion Electron Reverse Engineering: Clipboard — REA" />
<!-- prettier-ignore -->
<meta property="og:description" content="Follow Notion&#x27;s clipboard API through Electron&#x27;s preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format." />
<!-- prettier-ignore -->
<meta property="og:url" content="https://rea.tools/showcase/notion/" />
<!-- prettier-ignore -->
<meta property="og:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta property="og:image:type" content="image/png" />
<!-- prettier-ignore -->
<meta property="og:image:width" content="1200" />
<!-- prettier-ignore -->
<meta property="og:image:height" content="630" />
<!-- prettier-ignore -->
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- prettier-ignore -->
<meta name="twitter:card" content="summary_large_image" />
<!-- prettier-ignore -->
<meta name="twitter:title" content="Notion Electron Reverse Engineering: Clipboard — REA" />
<!-- prettier-ignore -->
<meta name="twitter:description" content="Follow Notion&#x27;s clipboard API through Electron&#x27;s preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format." />
<!-- prettier-ignore -->
<meta name="twitter:image" content="https://rea.tools/assets/social-card.png" />
<!-- prettier-ignore -->
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta
name="theme-color"
content="#ffffff"
media="(prefers-color-scheme: light)"
/>
<meta
name="theme-color"
content="#1e1e2e"
media="(prefers-color-scheme: dark)"
/>
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
<script src="../../assets/main.js" defer></script>
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- prettier-ignore -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
><span class="brand-name">REA</span
><span class="brand-caption" data-i18n-skip>Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/" aria-current="page">Showcases</a>
<a href="../../blog/">Blog</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
<div class="header-social">
<details class="language-picker" data-i18n-skip><summary><span aria-hidden="true">文 / A</span> <span>English</span></summary><nav aria-label="English"><a href="../../showcase/notion/" lang="en" dir="ltr" data-language="en" aria-current="true">English</a><a href="../../zh-CN/showcase/notion/" lang="zh-CN" dir="ltr" data-language="zh-CN">简体中文</a><a href="../../zh-TW/showcase/notion/" lang="zh-TW" dir="ltr" data-language="zh-TW">繁體中文</a><a href="../../ja/showcase/notion/" lang="ja" dir="ltr" data-language="ja">日本語</a><a href="../../ko/showcase/notion/" lang="ko" dir="ltr" data-language="ko">한국어</a><a href="../../tr/showcase/notion/" lang="tr" dir="ltr" data-language="tr">Türkçe</a><a href="../../ru/showcase/notion/" lang="ru" dir="ltr" data-language="ru">Русский</a><a href="../../vi/showcase/notion/" lang="vi" dir="ltr" data-language="vi">Tiếng Việt</a><a href="../../th/showcase/notion/" lang="th" dir="ltr" data-language="th">ไทย</a><a href="../../de/showcase/notion/" lang="de" dir="ltr" data-language="de">Deutsch</a><a href="../../es/showcase/notion/" lang="es" dir="ltr" data-language="es">Español</a><a href="../../fr/showcase/notion/" lang="fr" dir="ltr" data-language="fr">Français</a><a href="../../uk/showcase/notion/" lang="uk" dir="ltr" data-language="uk">Українська</a><a href="../../pl/showcase/notion/" lang="pl" dir="ltr" data-language="pl">Polski</a><a href="../../pt-BR/showcase/notion/" lang="pt-BR" dir="ltr" data-language="pt-BR">Português (Brasil)</a><a href="../../ar/showcase/notion/" lang="ar" dir="rtl" data-language="ar">العربية</a><a href="../../fa/showcase/notion/" lang="fa" dir="rtl" data-language="fa">فارسی</a><a href="../../id/showcase/notion/" lang="id" dir="ltr" data-language="id">Bahasa Indonesia</a><a href="../../it/showcase/notion/" lang="it" dir="ltr" data-language="it">Italiano</a></nav></details>
<a
class="nav-github"
href="https://github.com/morluto/rea"
aria-label="REA on GitHub"
title="REA on GitHub"
>
<svg
width="22"
height="22"
viewBox="0 0 16 16"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.65 7.65 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"
/>
</svg>
<span class="nav-github-count" hidden aria-hidden="true"></span>
</a>
<a
class="nav-discord"
href="https://discord.gg/GkcryMnJDM"
aria-label="Join REA on Discord"
title="Join REA on Discord"
>
<svg
width="22"
height="22"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286ZM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
<a href="../../">Home</a> / <a href="../">Showcases</a> / Notion
</p>
<p class="eyebrow">Case study · Electron</p>
<h1>How Notion copies<br />text and HTML.</h1>
<p class="lede">
Trace a clipboard call from Notion's page API, through the preload and
main process, to Electron's clipboard writer.
</p>
<p class="page-meta">Notion Desktop 7.6.1 · JavaScript · REA</p>
</div>
<figure class="workflow-figure">
<p class="figure-title">The bridge inside Notion's desktop package</p>
<ol class="flow-steps notion-bridge-flow">
<li>
<span class="flow-label">01 · Page-facing API</span>
<span class="flow-node"><code>clipboard.write(data)</code></span>
<span class="flow-note"
>The preload exposes a callable method.</span
>
</li>
<li>
<span class="flow-label">02 · Preload wrapper</span>
<span class="flow-node"><code>ipcRenderer.invoke</code></span>
<span class="flow-note"
>The wrapper sends the channel and arguments.</span
>
</li>
<li>
<span class="flow-label">03 · Main-process handler</span>
<span class="flow-node"><code>ipcMain.handle</code></span>
<span class="flow-note"
>The receiver checks which page sent it.</span
>
</li>
<li>
<span class="flow-label">04 · System clipboard</span>
<span class="flow-node"><code>clipboard.write(data)</code></span>
<span class="flow-note"
>Electron writes the supplied text and HTML.</span
>
</li>
</ol>
<p class="flow-result">
One channel joins the two sides:
<code class="notion-channel">notion:clipboard:write</code>
</p>
<figcaption>
The packaged code uses the same channel in the preload and main
process. Resolving the wrappers reveals the Electron calls behind
those names.
</figcaption>
</figure>
<div class="article-layout">
<nav class="contents" aria-label="On this page">
<p>In this case study</p>
<ul>
<li><a href="#clipboard">Follow the clipboard bridge</a></li>
<li><a href="#rich-clipboard">Keeping a block's structure</a></li>
<li><a href="#continue">More examples and sources</a></li>
</ul>
</nav>
<article class="article" aria-label="Notion Electron case study">
<section class="article-section" id="clipboard">
<h2>Where does the copy operation go?</h2>
<p>
The page sees a method called <code>clipboard.write</code>. Its
implementation in the preload is a wrapper. The useful question is
what that wrapper sends, and which code receives it.
</p>
<div class="agent-terminal">
<div class="terminal-header">
<span class="terminal-title"
><span class="terminal-dots" aria-hidden="true"
><i></i><i></i><i></i></span
>Your coding agent</span
><button
class="copy-button"
type="button"
data-copy="notion-prompt"
aria-label="Copy Notion analysis prompt"
>
Copy
</button>
</div>
<p class="terminal-prompt">
<span class="prompt-symbol" aria-hidden="true">›</span
><span id="notion-prompt"
>Use REA to trace how Notion copies text and HTML to the
system clipboard. Show the code at each step.</span
><span class="terminal-cursor" aria-hidden="true"></span>
</p>
</div>
<p class="caption">
An example prompt with Notion's local application package
available to your coding agent.
</p>
<h3 class="small-heading">REA finds where to start reading</h3>
<p>
Analyzing the tab preload with REA locates the API exposed to the
page and the call that sends an IPC request. Both findings come
with source ranges in the minified bundle:
</p>
<figure class="workflow-figure">
<p class="figure-title">REA findings → two places to open</p>
<div class="rea-query-pair">
<div class="code-block">
<div class="code-header"><span>Exposed page API</span></div>
<pre><code>api_key: "__electronApi"
api_status: "dynamic"
source: preload.js:1:22764–22828</code></pre>
</div>
<div class="code-block">
<div class="code-header"><span>Renderer IPC call</span></div>
<pre><code>operation: "invoke"
channel_expression: "e"
source: preload.js:1:6486–6522</code></pre>
</div>
</div>
<figcaption>
Selected fields from a fresh
<code>analyze_javascript_application</code> result on Notion's
tab preload, recorded with REA 4.1.0. Positions use the original
bundle's line and zero-based column.
</figcaption>
</figure>
<p>
The agent opens those locations, finds the clipboard member in the
assembled API, and reads <code>invokerInMain</code>. Its call site
supplies
<code class="notion-channel">notion:clipboard:write</code>. That
name gives the agent something precise to look for in the main
bundle. The four excerpts below show the route it connects.
</p>
<h3 class="small-heading">Four short pieces of code</h3>
<p class="caption">
Simplified from the packaged JavaScript, with readable variable
names. Unrelated members are omitted.
</p>
<ol class="rea-queries">
<li>
<h4>Start at the API exposed to the page</h4>
<div class="code-block">
<div class="code-header">
<span
>tab_browser_view/<span translate="no">preload.js</span> ·
API member</span
><button
class="copy-button"
type="button"
data-copy="notion-preload-source"
aria-label="Copy Notion preload excerpt"
>
Copy
</button>
</div>
<pre><code id="notion-preload-source">const clipboardApi = {
write: invokerInMain(<span class="notion-channel">"notion:clipboard:write"</span>)
};
// Included in the API exposed through contextBridge:
// window.__electronApi.clipboard</code></pre>
</div>
<p>
This member supplies the channel name. The implementation of
<code>invokerInMain</code> tells us how the request leaves the
page's process.
</p>
</li>
<li>
<h4>Resolve the preload's wrapper</h4>
<div class="code-block">
<div class="code-header">
<span
>tab_browser_view/<span translate="no">preload.js</span> ·
IPC helper</span
><button
class="copy-button"
type="button"
data-copy="notion-invoke-source"
aria-label="Copy Notion invoke wrapper excerpt"
>
Copy
</button>
</div>
<pre><code id="notion-invoke-source">function invokerInMain(channel) {
return (...args) =&gt; invokeInMainAndReturnResult(channel, ...args);
}
function invokeInMainAndReturnResult(channel, ...args) {
return ipcRenderer.invoke(channel, ...args);
}</code></pre>
</div>
<p>
The returned function forwards the arguments through
Electron's request-and-response IPC. We can now look for
<code class="notion-channel">notion:clipboard:write</code>
on the main-process side.
</p>
</li>
<li>
<h4>Check how the receiver is registered</h4>
<div class="code-block">
<div class="code-header">
<span
>main/<span translate="no">index.js</span> · handler
helper</span
><button
class="copy-button"
type="button"
data-copy="notion-handler-source"
aria-label="Copy Notion handler wrapper excerpt"
>
Copy
</button>
</div>
<pre><code id="notion-handler-source">const handleRequestFromRenderer = {
addListener(channel, handler) {
ipcMain.handle(channel, handler);
}
};</code></pre>
</div>
<p>
The main process uses another wrapper. Its
<code>addListener</code> method registers an
<code>ipcMain.handle</code> receiver for that channel.
</p>
</li>
<li>
<h4>Find the write and its sender check</h4>
<div class="code-block">
<div class="code-header">
<span
>main/<span translate="no">index.js</span> · clipboard
receiver</span
><button
class="copy-button"
type="button"
data-copy="notion-write-source"
aria-label="Copy Notion clipboard receiver excerpt"
>
Copy
</button>
</div>
<pre><code id="notion-write-source">handleRequestFromRenderer.addListener(
<span class="notion-channel">"notion:clipboard:write"</span>,
(event, data) =&gt;
isNotionWebContents(event.sender)
? clipboard.write(data)
: undefined
);</code></pre>
</div>
<p>
This is the destination. After checking the sender, the
handler passes the supplied data to Electron's clipboard API.
The channel and the two resolved wrappers connect the exposed
method to this call.
</p>
</li>
</ol>
</section>
<section class="article-section" id="rich-clipboard">
<h2>How does a copied block keep its structure?</h2>
<p>
Notion's web editor sends text and HTML through
<code>electronApi.clipboard.write</code>. It keeps the block data
locally and puts an ID in the copied HTML to connect them.
</p>
<figure class="workflow-figure">
<p class="figure-title">HTML links to saved block data</p>
<div class="notion-clipboard-grid">
<div class="code-block">
<div class="code-header">
<span>Sent to the system clipboard</span>
</div>
<pre><code>text: "Project notes"
html: "&lt;p&gt;Project notes&lt;/p&gt;"
+ "&lt;!-- notionvc: <span class="notion-channel">&lt;copy-id&gt;</span> --&gt;"</code></pre>
</div>
<div class="code-block">
<div class="code-header">
<span>Saved locally by Notion</span>
</div>
<pre><code>id: <span class="notion-channel">&lt;copy-id&gt;</span>
mimeTypes:
text/_notion-blocks-v3-production
→ structured block data</code></pre>
</div>
</div>
<ol
class="notion-mini-flow"
aria-label="Restoring rich clipboard data on paste"
>
<li>Read the HTML ID</li>
<li>Find the same local ID</li>
<li>Restore Notion block data</li>
</ol>
<figcaption>
Illustrative text and ID. The recorded module test restores the
block data when IDs match; a stale entry returns only text and
HTML.
</figcaption>
</figure>
<p>
On paste, Notion looks for the HTML ID and checks it against the
saved copy. Matching IDs restore the block data.
</p>
<p class="caption">
Clipboard format recorded from web assets cached on July 13, 2026.
</p>
</section>
<section class="article-section" id="continue">
<h2>More examples and sources</h2>
<details class="evidence-details" id="markdown">
<summary>Another example: Markdown tables</summary>
<p>
Notion parses Markdown into block data. This example shows how a
table becomes header and data rows.
</p>
<figure class="workflow-figure">
<p class="figure-title">
A table through Notion's Markdown parser
</p>
<div class="notion-table-example">
<div class="code-block">
<div class="code-header"><span>Markdown input</span></div>
<pre><code>| A | B |
| - | - |
| 1 | 2 |</code></pre>
</div>
<div class="code-block">
<div class="code-header">
<span>Structured parser output · excerpt</span>
</div>
<pre><code>table · headerRow: true
table_row
cells: ["A"], ["B"]
table_row
cells: ["1"], ["2"]</code></pre>
</div>
</div>
<figcaption>
Recorded output from an extracted Markdown parser. The table
becomes rows and cells.
</figcaption>
</figure>
</details>
<details class="evidence-details">
<summary>Another path: the offline SQLite connection</summary>
<p>
Notion's renderer requests <code>getSqliteMeta</code> through
IPC. The main process creates a message channel and gives one
port to the renderer and the other to a SQLite utility process.
Subsequent statement batches and results travel over that port.
</p>
<p>
Following this handoff explains where the database work runs,
and why tracing only the initial IPC request misses the later
conversation.
</p>
</details>
<details class="evidence-details">
<summary>Sources and how to read the excerpts</summary>
<p>
The clipboard bridge comes from Notion Desktop 7.6.1's
<code>app.asar</code>: the tab preload and
<code>main/index.js</code>. The excerpts expand minified
variable names and omit unrelated code; the sender-check helper
is shown with its underlying
<code>isNotionWebContents</code> call.
</p>
<p>
The virtual clipboard and Markdown examples come from web assets
cached on July 13, 2026, including modules <code>747001</code>,
<code>58169</code>, <code>736810</code> and <code>422457</code>.
Those assets have their own version. Saved probes executed
extracted modules and small harnesses; their outputs describe
those probes rather than a captured desktop UI session.
</p>
<p>
The REA result shown here is a focused analysis of the saved tab
preload, recorded with the published REA 4.1.0 package. It
locates the API and invoke operation. The clipboard member and
matching receiver are connected by reading their wrapper
implementations in the package.
</p>
<p>
The Markdown example uses parser module <code>422457</code>.
Saved module tests show that both page and chat filters preserve
this table. The paste/import, structured Markdown and AI/chat
parsers have separate entry points and intermediate formats.
</p>
</details>
<ul class="reading-list">
<li>
<a href="../../guides/javascript/"
>JavaScript application guide</a
><span
>Follow a small Electron application's export feature.</span
>
</li>
<li>
<a href="../dx-ball/">DX-Ball reconstruction</a
><span
>Follow native instructions to a checked C function.</span
>
</li>
</ul>
<div class="actions">
<a class="button" href="../../get-started/"
>Analyze your own app</a
>
</div>
</section>
</article>
</div>
</main>
<!-- website:footer:start -->
<!-- prettier-ignore -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
<div class="footer-links">
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE">MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>