board: a tag in the prose is coloured where it sits, not printed twice
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 10s
CI & Build / integration (push) Successful in 16s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 2m37s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m53s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Canceled after 3m25s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 10s
CI & Build / integration (push) Successful in 16s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 2m37s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m53s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Canceled after 3m25s
A tagged note was showing its tag twice — once where it was typed, once as a chip — and the duplicate was the loud copy. Now the chip row carries only what the body cannot say (a tag lifted off its own line, a label from the picker), and a `#tag` left mid-sentence is tinted in place. Which characters are a tag is asked of the CORE, the way the card already asks it which lines are checklist items: `extract_tag_spans` keeps the spans `extract_tags` throws away, and `body_tags` hands them to Kotlin. Offsets are UTF-16 code units, because `AnnotatedString` and JS both index that way and a char index lands mid-token the first time somebody writes an emoji. The web keeps its own matcher in markdown.ts, mirroring `line_tags` case for case. The inline ink is its own table, one Tailwind step deeper than the chip's. A chip brings its own -100 fill and reads against that alone; inline text sits on whatever the card is, including a gray-tagged card at neutral-200 — where the chip's -700 measured 3.98 (green), 4.11 (orange) and 4.34 (teal), under the 4.5 body text needs. At -800/-300 every hue lands 5.63-12.01 light and 7.20-10.84 dark across every palette and generated fill. Chips now carry the `#` on every surface. The via_tag branch that used to decide it is gone from the card, and Android's row said no hash at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,62 @@ pub fn extract_tags(body: &str) -> Vec<String> {
|
||||
out
|
||||
}
|
||||
|
||||
/// One `#tag` and exactly where it sits, for a renderer drawing the body itself.
|
||||
///
|
||||
/// The card no longer prints a chip for a tag whose text is still in the note — it
|
||||
/// colours the token where it was typed instead. To do that a renderer needs the
|
||||
/// SPAN, not just the name, and asking it to find the name again would be a second
|
||||
/// grammar quietly disagreeing with this one about what `##a` or `#1` is.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DerivedTag {
|
||||
/// Which body line it sits on, like [`DerivedItem::line`].
|
||||
pub line: u32,
|
||||
/// Offsets into that line, in UTF-16 code units — INCLUDING the leading `#`.
|
||||
///
|
||||
/// UTF-16 rather than chars or bytes because the two languages that consume this
|
||||
/// both index strings that way: Kotlin's `AnnotatedString` and JavaScript. A char
|
||||
/// index is right up until somebody puts an emoji before a tag, and then it lands
|
||||
/// mid-token with no error anywhere.
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Every `#tag` in `body` with its position — the same scan [`extract_tags`] does,
|
||||
/// keeping the spans instead of throwing them away.
|
||||
///
|
||||
/// Not deduped, unlike `extract_tags`: two mentions of `#todo` are two pieces of text
|
||||
/// to colour. Fences are not skipped either, and that is deliberate — `extract_tags`
|
||||
/// does not skip them, so a `#tag` inside a code block IS a label on the note, and a
|
||||
/// renderer that left it plain would be the only surface disagreeing.
|
||||
pub fn extract_tag_spans(body: &str) -> Vec<DerivedTag> {
|
||||
let mut out = Vec::new();
|
||||
for (n, line) in body.split('\n').enumerate() {
|
||||
let chars: Vec<char> = line.chars().collect();
|
||||
let spans = line_tags(&chars);
|
||||
if spans.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// Prefix sums, built once per tagged line: char index -> UTF-16 offset.
|
||||
let mut units: Vec<u32> = Vec::with_capacity(chars.len() + 1);
|
||||
let mut total: u32 = 0;
|
||||
units.push(0);
|
||||
for c in &chars {
|
||||
total += c.len_utf16() as u32;
|
||||
units.push(total);
|
||||
}
|
||||
for (start, end, name) in spans {
|
||||
out.push(DerivedTag {
|
||||
line: n as u32,
|
||||
start: units[start],
|
||||
end: units[end],
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Whether a line opens or closes a fenced code block.
|
||||
fn is_fence(line: &str) -> bool {
|
||||
let trimmed = line.trim_start();
|
||||
@@ -438,6 +494,36 @@ mod tests {
|
||||
assert!(extract_tags("").is_empty());
|
||||
}
|
||||
|
||||
// ── tag spans, for the renderer that draws them in place ─────────────────
|
||||
|
||||
#[test]
|
||||
fn tag_spans_carry_the_hash_and_the_line() {
|
||||
let spans = extract_tag_spans("buy milk #grocery\nand call #mom about #mom");
|
||||
assert_eq!(spans.len(), 3);
|
||||
assert_eq!((spans[0].line, spans[0].start, spans[0].end), (0, 9, 17));
|
||||
assert_eq!(spans[0].name, "grocery");
|
||||
// Not deduped: two mentions are two pieces of text to colour.
|
||||
assert_eq!(spans[1].line, 1);
|
||||
assert_eq!(spans[2].name, "mom");
|
||||
assert_eq!((spans[2].start, spans[2].end), (20, 24));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tag_spans_are_utf16_offsets_not_char_indices() {
|
||||
// The emoji is ONE char and TWO UTF-16 code units. Kotlin and JS both index
|
||||
// the second way, so a char index would highlight one character too early.
|
||||
let spans = extract_tag_spans("🎁 #gift");
|
||||
assert_eq!(spans.len(), 1);
|
||||
assert_eq!((spans[0].start, spans[0].end), (3, 8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tag_spans_agree_with_extract_tags_about_what_a_tag_is() {
|
||||
let body = "#1 nope a#b no but #Yes ##no";
|
||||
let names: Vec<String> = extract_tag_spans(body).into_iter().map(|t| t.name).collect();
|
||||
assert_eq!(names, extract_tags(body));
|
||||
}
|
||||
|
||||
// ── lifting standalone tags ──────────────────────────────────────────────
|
||||
//
|
||||
// The MIRROR of `split_body_tags` in the server's notes/tags.py, case for case.
|
||||
|
||||
Reference in New Issue
Block a user