Keep a pasted note whole, and search the notes from the app
Two things found by using this on real notes. **The paste.** Copying out of Apple Notes put most of the note on the floor. WebKit wraps a copied selection in a single span carrying the computed style of everything in it — `font-weight: 700` included — with the real blocks nested inside. The serializer read that span as inline, so every line collapsed into one paragraph and every word came out bold; switching to the Markdown view then showed what little had survived, which is what "most of the text was gone" was. And because the boldness came from a foreign span's style rather than a tag, the bold button could not remove it. The rule now is that an element holding blocks is a block whatever its tag, and that a container's style is not emphasis — only a span wrapping a single run of text is. A paste this editor cannot read at all (some engines withhold the clipboard from the event) is tidied afterwards instead, but only if something actually arrived, so an empty paste still costs nothing. **The search.** A Suche tab over the user's own notes, reading the files rather than the index: notes reach the index only on a full crawl, so a lesson written this morning would not be findable this morning, which is most of what anyone searches their own notes for. A result names the lesson it matched in, not the day, for the same reason the index indexes day notes per section. Tapping one opens that day in the editor. Driven in Firefox against the real app with a proxied session: the paste, six switches between the two views, bold and unbold on pasted text, the search, and opening a result. 386 unit tests, 114/115 smoke. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -384,7 +384,13 @@ function serializeBlocks(node) {
|
||||
};
|
||||
|
||||
for (const child of children(node)) {
|
||||
if (child.nodeType === 1 && BLOCK_TAGS.has(child.nodeName)) {
|
||||
// A block *inside* an inline element is still a block. WebKit wraps a
|
||||
// copied selection in one span carrying the computed style of everything
|
||||
// in it, so a paste from Apple Notes arrives as
|
||||
// `<span style="font-weight: 700"><div>…</div><div>…</div></span>` —
|
||||
// and reading that span as inline flattened a whole note into one
|
||||
// paragraph and made every word of it bold.
|
||||
if (child.nodeType === 1 && (BLOCK_TAGS.has(child.nodeName) || holdsBlock(child))) {
|
||||
flush();
|
||||
const block = serializeBlock(child);
|
||||
if (block) out.push(block);
|
||||
@@ -438,7 +444,10 @@ function serializeBlock(element) {
|
||||
// asking what is inside.
|
||||
return hasBlockChild(element) ? serializeBlocks(element) : paragraph(inlineFrom(children(element)));
|
||||
default:
|
||||
return paragraph(inlineFrom(children(element)));
|
||||
// Anything else that reached this function is here because it holds
|
||||
// blocks — a paste wrapper, most often. Its own tag means nothing;
|
||||
// what it contains means everything.
|
||||
return holdsBlock(element) ? serializeBlocks(element) : paragraph(inlineFrom(children(element)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,9 +581,15 @@ function inlineNode(node) {
|
||||
return emphasise(inlineFrom(children(node)), '~~');
|
||||
case 'SPAN':
|
||||
case 'FONT': {
|
||||
// What a paste leaves behind. The tag says nothing; the style might.
|
||||
const style = node.getAttribute('style') ?? '';
|
||||
// What a paste leaves behind. The tag says nothing; the style might
|
||||
// — but only for a span wrapping one run of text. A span with
|
||||
// elements inside it is a container carrying inherited style, not
|
||||
// emphasis: WebKit hangs the whole computed style of a copied
|
||||
// selection on such a wrapper, and honouring its `font-weight: 700`
|
||||
// is what made an entire pasted note bold.
|
||||
const inner = inlineFrom(children(node));
|
||||
if (!isTextOnly(node)) return inner;
|
||||
const style = node.getAttribute('style') ?? '';
|
||||
if (/font-weight:\s*(bold|[6-9]00)/i.test(style)) return emphasise(inner, '**');
|
||||
if (/font-style:\s*italic/i.test(style)) return emphasise(inner, '_');
|
||||
return inner;
|
||||
@@ -647,6 +662,26 @@ function hasBlockChild(element) {
|
||||
return children(element).some((child) => child.nodeType === 1 && BLOCK_TAGS.has(child.nodeName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a block hides anywhere under this element.
|
||||
*
|
||||
* Pastes nest wrappers several deep — `<span><span><div>` — so the answer has
|
||||
* to be looked for rather than checked one level down. Bounded, because the
|
||||
* tree comes from a clipboard and nothing here should be able to hang on one.
|
||||
*/
|
||||
function holdsBlock(element, depth = 0) {
|
||||
if (depth > 6) return false;
|
||||
return children(element).some(
|
||||
(child) =>
|
||||
child.nodeType === 1 && (BLOCK_TAGS.has(child.nodeName) || child.nodeName === 'LI' || holdsBlock(child, depth + 1)),
|
||||
);
|
||||
}
|
||||
|
||||
/** A span with nothing but text in it — the only shape whose style is emphasis. */
|
||||
function isTextOnly(element) {
|
||||
return children(element).every((child) => child.nodeType === 3 || child.nodeName === 'BR');
|
||||
}
|
||||
|
||||
function children(node) {
|
||||
return Array.prototype.slice.call(node.childNodes ?? []);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user