Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/test/test_preview_stray_backslash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import unittest

import pandas as pd

from vfbquery.vfb_queries import encode_markdown_links


class PreviewStrayBackslashTest(unittest.TestCase):
"""Query preview rows are assembled in vfb_queries.py from raw Neo4j labels
(via apoc.text.format), NOT through term_info_queries.clean_label. So the
stray backslash-before-quote artifact that PR #80 stripped from a term's own
labels still reached the ``name`` / ``thumbnail`` markdown cells of query
previews when an embedded neuron label contained an apostrophe (e.g. the
NBLAST 'similar neurons' preview showing ``MBON01(y5B\\'2a)_R``).

encode_markdown_links is the shared 'prepare a markdown cell for display'
pass for those columns, so the escape stripping lives there. These tests are
network-free: they exercise the encoder directly on production-shaped
(object-dtype) frames.
"""

def _obj_df(self, data):
# Match the dtype the live pipeline produces (pd.DataFrame.from_records
# of dict rows yields object columns). A str/StringDtype column would be
# skipped by the encoder's ``dtype != object`` guard.
return pd.DataFrame({k: pd.Series(v, dtype=object) for k, v in data.items()})

def test_link_cell_stray_backslash_stripped(self):
raw = "[MBON01(y5B\\'2a)_R (FlyEM-HB:612371421)](VFB_jrchk0e2)"
df = self._obj_df({"name": [raw]})
out = encode_markdown_links(df, ["name"])
val = out["name"].iloc[0]
self.assertNotIn("\\", val)
self.assertEqual(
"[MBON01(y5B'2a)_R (FlyEM-HB:612371421)](VFB_jrchk0e2)", val
)

def test_image_cell_stray_backslash_stripped(self):
# https URL (as produced by the Cypher REPLACE) so the only thing that
# should change is the removal of the stray backslashes in alt + title.
raw = (
"[![MBON01(y5B\\'2a)_R aligned to JRCFIB2018Fum]"
"(https://www.virtualflybrain.org/data/x/thumbnail.png "
"'MBON01(y5B\\'2a)_R aligned to JRCFIB2018Fum')](t,VFB_jrchk0e2)"
)
df = self._obj_df({"thumbnail": [raw]})
out = encode_markdown_links(df, ["thumbnail"])
val = out["thumbnail"].iloc[0]
self.assertNotIn("\\", val)
self.assertIn("MBON01(y5B'2a)_R", val)

def test_clean_cells_untouched(self):
raw = "[MBON01_L](VFB_00000001)"
df = self._obj_df({"name": [raw]})
out = encode_markdown_links(df, ["name"])
self.assertEqual(raw, out["name"].iloc[0])

def test_null_cells_preserved(self):
df = self._obj_df({"name": [None, "[a\\'b](x)"]})
out = encode_markdown_links(df, ["name"])
self.assertTrue(pd.isna(out["name"].iloc[0]))
self.assertNotIn("\\", out["name"].iloc[1])


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion src/vfbquery/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
cache's version stamp can never drift apart.
"""

__version__ = "1.22.32"
__version__ = "1.22.33"
20 changes: 20 additions & 0 deletions src/vfbquery/vfb_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,15 @@ def encode_brackets(text):
# markdown parser.
_RE_MD_LINK = re.compile(r'\[([^\]]+)\]\(([^\)]+)\)')

# Stray backslash-before-quote artifact (e.g. y5B\'2a -> y5B'2a). The source
# label data carries a spurious backslash before apostrophes/quotes; it must be
# stripped before it reaches a display cell (and, for link cells, before it can
# leak into an HTTP request target where a backslash is illegal). Mirrors
# term_info_queries.clean_label so a term's own labels and the labels embedded
# in query preview-row markdown (name / thumbnail / label cells, built here from
# raw Neo4j labels via apoc.text.format) are cleaned identically.
_STRAY_QUOTE_ESCAPE = re.compile(r"\\(['\"])")


def _encode_image_url(match: 're.Match') -> str:
"""Repl callback for image-markdown cells — secure URL, preserve rest."""
Expand Down Expand Up @@ -479,6 +488,17 @@ def encode_markdown_links(df, columns):
continue
non_null = s.where(notnull_mask, '').astype(str)

# Strip stray backslash-before-quote artifacts from every cell first, so
# embedded labels (link text, image alt text, titles) are cleaned in
# both the image-markdown and label-shape branches below. PR #80 cleaned
# the term's own labels via term_info_queries.clean_label, but query
# preview rows are assembled here from raw Neo4j labels and never passed
# through that path, so the escape survived into name/thumbnail cells.
# Vectorised + guarded on a cheap literal-backslash check so it stays
# essentially free on the large frames this function was tuned for.
if non_null.str.contains('\\', regex=False, na=False).any():
non_null = non_null.str.replace(_STRAY_QUOTE_ESCAPE, r"\1", regex=True)

# Branch on shape using a vectorised prefix check — this is a single
# C-loop over the column, far cheaper than re-detecting per row.
is_image = non_null.str.startswith('[![', na=False)
Expand Down
Loading