mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-07-21 09:02:54 +02:00
web: fix emoji pattern render in short names (#760)
* web: fix emoji pattern render in short names * web: address review comments
This commit is contained in:
@@ -71,6 +71,13 @@ module PotatoMesh
|
||||
# Symbols and Dingbats (U+2600–U+27BF), and Miscellaneous Symbols and
|
||||
# Arrows (U+2B00–U+2BFF).
|
||||
#
|
||||
# Matching is intentionally single-codepoint: callers iterate grapheme
|
||||
# clusters first and then test each cluster against this pattern, so
|
||||
# multi-codepoint emoji (country flags like 🇩🇪 = 🇩 + 🇪, ZWJ family
|
||||
# sequences like 👨👩👧, skin-tone modifiers like 👍🏽, the rainbow flag
|
||||
# 🏳️🌈) come through intact instead of being shredded into their
|
||||
# component codepoints.
|
||||
#
|
||||
# @type [Regexp]
|
||||
MESHCORE_COMPANION_EMOJI_PATTERN = /[\u{1F000}-\u{1FFFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}]/u
|
||||
|
||||
@@ -79,16 +86,20 @@ module PotatoMesh
|
||||
# richer, human-readable variant for the API layer without touching the DB.
|
||||
#
|
||||
# Algorithm (applied in priority order):
|
||||
# 1. If the long name contains an emoji character (see
|
||||
# +MESHCORE_COMPANION_EMOJI_PATTERN+), use the first emoji embedded in a
|
||||
# 4-column display slot: ``" E "`` (one leading space, emoji, one trailing
|
||||
# space). Emoji are rendered double-width in monospace fonts, so one leading
|
||||
# space keeps the badge at four visual columns.
|
||||
# 2. If the long name contains two or more whitespace-separated words, use
|
||||
# the capitalised first letters of the first two words: ``" XY "``.
|
||||
# 3. Return +nil+ — single-word names fall back to the raw short name stored
|
||||
# in the database (typically the first two bytes of the node ID). A single
|
||||
# initial looked poor and carried no more information than the raw value.
|
||||
# 1. If the long name contains an emoji grapheme cluster (anchored by
|
||||
# +MESHCORE_COMPANION_EMOJI_PATTERN+), use that whole cluster in a
|
||||
# 4-column display slot: ``" E "`` (one leading space, emoji, one
|
||||
# trailing space). Emoji are rendered double-width in monospace fonts,
|
||||
# so one leading space keeps the badge at four visual columns.
|
||||
# Iterating grapheme clusters (rather than raw codepoints) preserves
|
||||
# multi-codepoint sequences such as country flags 🇩🇪, ZWJ families
|
||||
# 👨👩👧, and skin-tone-modified thumbs 👍🏽.
|
||||
# 2. If the long name contains two or more whitespace-separated words,
|
||||
# use the capitalised first letters of the first two words: ``" XY "``.
|
||||
# 3. Return +nil+ — single-word names fall back to the raw short name
|
||||
# stored in the database (typically the first two bytes of the node
|
||||
# ID). A single initial looked poor and carried no more information
|
||||
# than the raw value.
|
||||
#
|
||||
# @param long_name [String, nil] long name stored on the node.
|
||||
# @return [String, nil] derived display short name or +nil+.
|
||||
@@ -96,10 +107,12 @@ module PotatoMesh
|
||||
name = string_or_nil(long_name)
|
||||
return nil unless name
|
||||
|
||||
emoji = name.scan(MESHCORE_COMPANION_EMOJI_PATTERN).first
|
||||
emoji_cluster = name.each_grapheme_cluster.find do |cluster|
|
||||
cluster.match?(MESHCORE_COMPANION_EMOJI_PATTERN)
|
||||
end
|
||||
# Wide emoji occupies two display columns, so use one leading space and
|
||||
# one trailing space to stay within the four-column badge width.
|
||||
return " #{emoji} " if emoji
|
||||
return " #{emoji_cluster} " if emoji_cluster
|
||||
|
||||
words = name.strip.split(/\s+/).reject(&:empty?)
|
||||
return nil if words.empty?
|
||||
|
||||
@@ -206,5 +206,61 @@ RSpec.describe PotatoMesh::App::Helpers do
|
||||
it "returns nil for a single-word name with no emoji (falls back to raw DB short name)" do
|
||||
expect(helper.meshcore_companion_display_short_name("Zigzag")).to be_nil
|
||||
end
|
||||
|
||||
# Multi-codepoint emoji coverage — see the in-file comment on
|
||||
# +MESHCORE_COMPANION_EMOJI_PATTERN+ for the grapheme-cluster rationale.
|
||||
# Each of these cases shredded into its component codepoints before the
|
||||
# fix and would otherwise render as a stray regional-indicator letter, a
|
||||
# lone family member, or an unadorned thumbs-up.
|
||||
|
||||
it "preserves a country-flag grapheme cluster (🇩🇪) instead of emitting just the first regional indicator" do
|
||||
name = "sidux.user \u{1F1E9}\u{1F1EA}"
|
||||
expect(
|
||||
helper.meshcore_companion_display_short_name(name),
|
||||
).to eq(" \u{1F1E9}\u{1F1EA} ")
|
||||
end
|
||||
|
||||
it "preserves a ZWJ family sequence (👨👩👧) as one cluster" do
|
||||
family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}"
|
||||
name = "Home #{family}"
|
||||
expect(
|
||||
helper.meshcore_companion_display_short_name(name),
|
||||
).to eq(" #{family} ")
|
||||
end
|
||||
|
||||
it "preserves a skin-tone-modified emoji (👍🏽) as one cluster" do
|
||||
thumb = "\u{1F44D}\u{1F3FD}"
|
||||
name = "Ack #{thumb}"
|
||||
expect(
|
||||
helper.meshcore_companion_display_short_name(name),
|
||||
).to eq(" #{thumb} ")
|
||||
end
|
||||
|
||||
it "preserves the rainbow-flag ZWJ sequence (🏳️🌈) as one cluster" do
|
||||
rainbow = "\u{1F3F3}\u{FE0F}\u{200D}\u{1F308}"
|
||||
name = "Pride #{rainbow}"
|
||||
expect(
|
||||
helper.meshcore_companion_display_short_name(name),
|
||||
).to eq(" #{rainbow} ")
|
||||
end
|
||||
|
||||
it "picks the first emoji cluster when a flag is followed back-to-back by a plain emoji" do
|
||||
# No separator between clusters — proves ``find`` stops at the flag's
|
||||
# grapheme cluster rather than splitting on a subsequent codepoint that
|
||||
# also falls in the pattern range.
|
||||
name = "\u{1F1E9}\u{1F1EA}\u{1F600}"
|
||||
expect(
|
||||
helper.meshcore_companion_display_short_name(name),
|
||||
).to eq(" \u{1F1E9}\u{1F1EA} ")
|
||||
end
|
||||
|
||||
it "returns the cluster when the long name is only an emoji and nothing else" do
|
||||
# Exercises the branch where the first cluster at index 0 matches and
|
||||
# there is no surrounding ASCII to drive the initials fallback.
|
||||
name = "\u{1F1E9}\u{1F1EA}"
|
||||
expect(
|
||||
helper.meshcore_companion_display_short_name(name),
|
||||
).to eq(" \u{1F1E9}\u{1F1EA} ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user