mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
* 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>
633 lines
30 KiB
HTML
633 lines
30 KiB
HTML
<!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's clipboard API through Electron'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's clipboard API through Electron'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) => 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) =>
|
||
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: "<p>Project notes</p>"
|
||
+ "<!-- notionvc: <span class="notion-channel"><copy-id></span> -->"</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"><copy-id></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>
|