Skip to content

Commit

Permalink
fix: instance expressions with double quotes not escaped / converted (X…
Browse files Browse the repository at this point in the history
…LSForm#709)

- like other functions, argument value(s) (instance name) should be
  allowed to be wrapped in single or double quotes. Previously only
  singles quotes worked.
  • Loading branch information
lindsay-stevens authored Jun 18, 2024
1 parent 1599d1f commit 59c37e0
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 10 deletions.
7 changes: 5 additions & 2 deletions pyxform/parsing/instance_expression.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re
from typing import TYPE_CHECKING

from pyxform.utils import BRACKETED_TAG_REGEX, EXPRESSION_LEXER, ExpLexerToken
from pyxform.utils import BRACKETED_TAG_REGEX, EXPRESSION_LEXER, ExpLexerToken, node

if TYPE_CHECKING:
from pyxform.survey import Survey
Expand Down Expand Up @@ -116,7 +116,10 @@ def replace_with_output(xml_text: str, context: "SurveyElement", survey: "Survey
lambda m: survey._var_repl_function(m, context),
old_str,
)
new_strings.append((start, end, old_str, f'<output value="{new_str}" />'))
# Generate a node so that character escapes are applied.
new_strings.append(
(start, end, old_str, node("output", value=new_str).toxml())
)
# Position-based replacement avoids strings which are substrings of other
# replacements being inserted incorrectly. Offset tracking deals with changing
# expression positions due to incremental replacement.
Expand Down
59 changes: 51 additions & 8 deletions tests/test_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Test the "note" question type.
"""

from dataclasses import dataclass
from dataclasses import dataclass, field

from tests.pyxform_test_case import PyxformTestCase
from tests.xpath_helpers.questions import xpq
Expand All @@ -15,8 +15,8 @@ class Case:
"""

label: str
xpath: str
match: set[str]
xpath: str = field(default_factory=lambda: xpq.body_input_label_output_value("note"))


class TestNotes(PyxformTestCase):
Expand Down Expand Up @@ -77,45 +77,88 @@ def test_instance_expression__permutations(self):
| | c2 | b | Big |
| | c2 | s | Small |
"""
# It's a bit confusing, but although double quotes are literally HTML in entity
# form (i.e. `&quot;`) in the output, for pyxform test comparisons they get
# converted back, so the expected output strings are double quotes not `&quot;`.
cases = [
# A pyxform token.
Case(
"${text}",
xpq.body_input_label_output_value("note"),
{" /test_name/text "},
),
# Instance expression with predicate using pyxform token and equals.
Case(
"instance('c1')/root/item[name = ${q1}]/label",
xpq.body_input_label_output_value("note"),
{"instance('c1')/root/item[name = /test_name/q1 ]/label"},
),
# Instance expression with predicate using pyxform token and equals (double quotes).
Case(
"""instance("c1")/root/item[name = ${q1}]/label""",
{"""instance("c1")/root/item[name = /test_name/q1 ]/label"""},
),
# Instance expression with predicate using pyxform token and function.
Case(
"instance('c2')/root/item[contains(name, ${q2})]/label",
xpq.body_input_label_output_value("note"),
{"instance('c2')/root/item[contains(name, /test_name/q2 )]/label"},
),
# Instance expression with predicate using pyxform token and function (double quotes).
Case(
"""instance("c2")/root/item[contains("name", ${q2})]/label""",
{"""instance("c2")/root/item[contains("name", /test_name/q2 )]/label"""},
),
# Instance expression with predicate using pyxform token and function (mixed quotes).
Case(
"""instance('c2')/root/item[contains("name", ${q2})]/label""",
{"""instance('c2')/root/item[contains("name", /test_name/q2 )]/label"""},
),
# Instance expression with predicate using pyxform token and equals.
Case(
"instance('c2')/root/item[contains(name, instance('c1')/root/item[name = ${q1}]/label)]/label",
xpq.body_input_label_output_value("note"),
{
"instance('c2')/root/item[contains(name, instance('c1')/root/item[name = /test_name/q1 ]/label)]/label"
},
),
# Instance expression with predicate using pyxform token and equals (double quotes).
Case(
"""instance("c2")/root/item[contains(name, instance("c1")/root/item[name = ${q1}]/label)]/label""",
{
"""instance("c2")/root/item[contains(name, instance("c1")/root/item[name = /test_name/q1 ]/label)]/label"""
},
),
# Instance expression with predicate using pyxform token and equals (mixed quotes).
Case(
"""instance('c2')/root/item[contains(name, instance("c1")/root/item[name = ${q1}]/label)]/label""",
{
"""instance('c2')/root/item[contains(name, instance("c1")/root/item[name = /test_name/q1 ]/label)]/label"""
},
),
# Instance expression with predicate not using a pyxform token.
Case(
"instance('c1')/root/item[name = 'y']/label",
xpq.body_input_label_output_value("note"),
{"instance('c1')/root/item[name = 'y']/label"},
),
# Instance expression with predicate not using a pyxform token (double quotes).
Case(
"""instance("c1")/root/item[name = "y"]/label""",
{"""instance("c1")/root/item[name = "y"]/label"""},
),
# Instance expression with predicate not using a pyxform token (mixed quotes).
Case(
"""instance("c1")/root/item[name = 'y']/label""",
{"""instance("c1")/root/item[name = 'y']/label"""},
),
# Instance expression with predicate not using a pyxform token (all escaped).
Case(
"""instance("c1")/root/item[name <> 1 and "<>&" = "1"]/label""",
{
"""instance("c1")/root/item[name &lt;&gt; 1 and "&lt;&gt;&amp;" = "1"]/label"""
},
),
]
wrap_scenarios = ("{}", "Text {}", "{} text", "Text {} text")
# All cases together in one.
combo_case = Case(
" ".join(c.label for c in cases),
xpq.body_input_label_output_value("note"),
{m for c in cases for m in c.match},
)
cases.append(combo_case)
Expand Down

0 comments on commit 59c37e0

Please sign in to comment.