Please adjust case changing functions to skip injected code

Hello!

It's possible for displayed strings to contain code in the form {=DoSomething()}. Templates can even include other Templates without needing to use the mildly less intuitive Dynamic Templates.

  <template name="ColorRed">#f23</template>
  <template name="ColorGreen">#4d2</template>

<!-- ... -->

  <template name="ColorON">{=Template("ColorGreen")}</template>
  <template name="ColorOFF">{=Template("ColorRed")}</template>

My code library uses this to allow game authors to quickly change colors in specific contexts while sticking to a predetermined color palette, giving a MUD-like feel.

However, because injected code is for all intents and purposes plain text until processed at display time, it is affected by case functions such as LCase(). Code is almost always case-sensitive, as are Template names, so this generally means that code injected into strings stops working when case functions are introduced:

Error running script: Error evaluating expression 'ToString(eval(section))': Error evaluating expression 'template("colorred")': No template named 'colorred'

Specifically, the contents of Template("ColorOFF") are faithfully pasted, but those contents are {=Template("ColorRed")}, which consequently becomes the all-lowercase template("colorred"), which produces an error as Template names are case-sensitive.

This can hypothetically be fixed if the case functions are altered to ignore all text between {= and }, thus no longer affecting injected code. If casing needs to be applied to said code, it can simply be repeated inside of that code, e.g.

This part of the string is affected by casing, but {=LCase("THIS PART ISN'T")}, so LCase() must be repeated.

In the meantime, I'll just have to redo the code to not bump into this problem...


Where are the case-changing functions being used? This looks to me like the it could be resolved by putting the case changing functions outside of other code.

Most likely, what you want to do is something like:

msg (LCase (ProcessText (somestring))

This runs ProcessText to replace all your {=Template…} sections with the contents of the template, and then calls LCase on the result, so there is no error.


If you really want to change the case of all text that is not part of a text processor directive, you could make a function like:

<function name="UCasePartial" parameters="input" type="string">
  output = ""
  depth = 0
  for (i, 1, LengthOf(input)) {
    char = Mid (input, i, 1)
    if (char = "{") depth = depth + 1
    else if (char = "}") depth = depth - 1
    if (depth = 0) char = UCase (char)
    output = output + char
  }
  return (output)
</function>

However, this also does not change the case of text inside the embedded template – it just skips all text processor directives.


Yeah. . .

LCase is just doing its one and only job: making everything you feed it lower case.

This is like saying, "I eat chili with beans, but I want my stomach to skip over the beans (as if they were corn) because I don't want to fart."

If you don't want to fart, but you do (for whatever reason) want whole beans in your toilet, just put the whole beans in the toilet instead of eating them.

If mrangel's solution isn't what you seek, click "Details" to see my dumb idea:


Here's a crazy idea, if you are only applying LCase to your strings:

Don't make your template names upper case.

<!--Saved by Quest 5.8.7753.35184-->
<asl version="580">
  <include ref="English.aslx" />
  <include ref="Core.aslx" />
  <template name="color_red">#f23</template>
  <template name="color_green">#4d2</template>
  <template name="color_on">{=Template("color_green")}</template>
  <template name="color_off">{=Template("color_red")}</template>
  <game name="template nesting">
    <gameid>4a25b53c-f065-428f-83c0-8e756c565ca6</gameid>
    <version>1.0</version>
    <firstpublished>2021</firstpublished>
  </game>
  <object name="room">
    <inherit name="editor_room" />
    <isroom />
    <enter type="script">
      msg (Template("color_red"))
      msg (Template("color_green"))
      msg (LCase(Template("color_on")))
      msg (LCase(Template("color_off")))
    </enter>
    <object name="player">
      <inherit name="editor_object" />
      <inherit name="editor_player" />
    </object>
  </object>
</asl>

Also, skimming over the code, wouldn't it be quicker to put:

  <template name="ColorON">[ColorGreen]</template>

rather than

  <template name="ColorON">{=Template("ColorGreen")}</template>

I've not tried it (don't have the desktop editor), but it looks to me like the [Template] construct should work recursively. And in this case, the template substitution happens when the template is parsed, rather than when it's sent to the player, so there's no problem with case folding.


(This question also makes me wonder why there isn't a text processor function for changing case; it seems like most of the dynamic templates in the language file are only dynamic because they call CapFirst ~ so having case functions (and possibly Conjugate) in the text processor would make them so much simpler)

Something like:

      <item key="uc:">
        game.textprocessorcommandresult = UCase (ProcessTextSection (Mid (section, 4), data))
      </item>
      <item key="lc:">
        game.textprocessorcommandresult = LCase (ProcessTextSection (Mid (section, 4), data))
      </item>
      <item key="cap:">
        game.textprocessorcommandresult = CapFirst (ProcessTextSection (Mid (section, 5), data))
      </item>

After testing this code while writing this post, I realized that none of this will help fight an author using UCase or LCase on a string containing a text processor directive.

You said:

My code library uses this to allow game authors to quickly change colors in specific contexts while sticking to a predetermined color palette

That makes me think you have no idea when someone might decide to use UCase or LCase on a string containing a text processor directive.

If that is the case, this may sound mean, buy they just need to learn (by trial and error, or by your example) when to use UCase or LCase on a string and when not to use UCase or LCase on a string.

I shall attempt to explain the issue (with no fart jokes this time).

The ProcessText function has 1 parameter, which is text.

The problem in your case is that text is being run through LCase by the author before sending text to ProcessText.

Now, ProcessText has no way to know what has happened to text (if anything) before text was passed to it.

There is no telling what got sent to ProcessText either. All sorts of things behind the scenes use ProcessText.

And we can't really add a line in ProcessText that changes specific characters back into upper case or lower case characters as needed, because (1) ProcessText doesn't know what anything is supposed to be and (2) Windows is not case-sensitive (neither is Quest on Windows), but the game running inside of Quest on Windows is case-sensitive because most of the code is running through the Chromium (open-source Chrome) browser (which is case-sensitive).

What am I rambling on about that for?

Well, I had a bright idea. I'd create a text-processor directive twice: one with an all lower-case name and the other with an all upper-case name. This would catch it no matter what an author did with UCase or LCase.

But silly me! Quest is not case-sensitive, therefore sees "COLOR_OFF" and "color_off" (as well as "CoLOr_oFf" and "cOloR_OfF") as the same text, and it throws an error about trying to create something that already exists. Ha ha! I am silly!

So, anyway, I tried all the things I could think of to intercept an author using UCase or LCase on a string containing a text processor directive before sending it to ProcessText so Quest could be updated to handle such things by default, but I don't think it can be done.

I haven't tested mrangel's code, but it looks like you could modify UCase and LCase in your library, modifying each as per mrangel's example if you wanted to, and then you wouldn't need to explain any extra custom steps to those using your library.


Or, you could add custom text processor directives.

https://docs.textadventures.co.uk/quest/text_processor.html#extending


Here is one example game.

I set the game's default text color to red. I created the ColorGreen template (just to try to stick close to your method).

I created two new text processor directives: color_on and color_off.

Here is an example line of code to print text with it:

msg ("{color_on}This color is on,{color_off}but this color is off.")

Here is an example room description that works as expected:

{color_on}{=LCase("THIS IS IN COLOR.")}{color_off} BUT THIS IS NOT IN COLOR.

{color_on}{=UCase("this is in color.")}{color_off} but this is not in color.

{color_on}This is a test to see if the color will work after a line break.

If this is still in color, it worked. {color_off}The color is now off.

EXAMPLE GAME

<!--Saved by Quest 5.8.7753.35184-->
<asl version="580">
  <include ref="English.aslx" />
  <include ref="Core.aslx" />
  <template name="ColorRed">#f23</template>
  <template name="ColorGreen">#4d2</template>
  <template name="ColorON">[ColorGreen]</template>
  <template name="ColorOFF">[ColorRed]</template>
  <game name="template nesting">
    <gameid>4a25b53c-f065-428f-83c0-8e756c565ca6</gameid>
    <version>0.2</version>
    <firstpublished>2021</firstpublished>
    <defaultforeground>Red</defaultforeground>
    <start type="script"><![CDATA[
      game.textprocessorcommands = game.textprocessorcommands
      scr => {
        s = Mid(section, 6)
        s = Replace (s, "COLOR_ON", "color_on")
        game.textprocessorcommandresult = "<span style=\"color:" + Template ("ColorGreen") + "\">"
      }
      dictionary add (game.textprocessorcommands, "color_on", scr)
      scr => {
        s = Mid(section, 6)
        s = Replace (s, "COLOR_OFF", "color_off")
        game.textprocessorcommandresult = "</span>"
      }
      dictionary add (game.textprocessorcommands, "color_off", scr)
    ]]></start>
  </game>
  <object name="room">
    <inherit name="editor_room" />
    <isroom />
    <description><![CDATA[{color_on}{=LCase("THIS IS IN COLOR.")}{color_off} BUT THIS IS NOT IN COLOR.<br/><br/>{color_on}{=UCase("this is in color.")}{color_off} but this is not in color.<br/><br/>{color_on}This is a test to see if the color will work after a line break.<br/><br/>If this is still in color, it worked. {color_off}The color is now off.]]></description>
    <object name="player">
      <inherit name="editor_object" />
      <inherit name="editor_player" />
    </object>
  </object>
</asl>

This question also makes me wonder why there isn't a text processor function for changing case; it seems like most of the dynamic templates in the language file are only dynamic because they call CapFirst ~ so having case functions (and possibly Conjugate) in the text processor would make them so much simpler

I was thinking this, too.

After I wrote the last post (which I was writing while you were writing your last post), I decided I was about to create text processor directives for upper- and lower-case.

I'm going to test it out now. (I am thinking there is a 50% chance it will alter the text of any nested text-processor directives and mess everything all up, though.)


Also, skimming over the code, wouldn't it be quicker to put: [...] rather than [...]

I've not tried it (don't have the desktop editor), but it looks to me like the [Template] construct should work recursively. And in this case, the template substitution happens when the template is parsed, rather than when it's sent to the player, so there's no problem with case folding.

Yes, sir. I just tested and approved this, and I updated my last example game using it.


UPDATED

Room description

{color_on}{lc:THIS IS IN COLOR.}{color_off} BUT THIS IS NOT IN COLOR.

{color_on}{uc:this is in color.}{color_off} but this is not in color.

{color_on}{cap:this} is a test to see if the color will work after a line break.

If this is still in color, it worked. {color_off}The color is now off.

image

EXAMPLE GAME

<!--Saved by Quest 5.8.7753.35184-->
<asl version="580">
  <include ref="English.aslx" />
  <include ref="Core.aslx" />
  <template name="ColorGreen">#4d2</template>
  <template name="ColorON">[ColorGreen]</template>
  <game name="template nesting">
    <gameid>4a25b53c-f065-428f-83c0-8e756c565ca6</gameid>
    <version>0.4</version>
    <firstpublished>2021</firstpublished>
    <defaultforeground>Red</defaultforeground>
    <start type="script"><![CDATA[
      game.textprocessorcommands = game.textprocessorcommands
      scr => {
        s = Mid(section, 6)
        s = Replace (s, "COLOR_ON", "color_on")
        // ^^^ just in case someone makes it upper-case before sending to ProcessText
        game.textprocessorcommandresult = "<span style=\"color:" + Template ("ColorGreen") + "\">"
      }
      dictionary add (game.textprocessorcommands, "color_on", scr)
      scr => {
        s = Mid(section, 6)
        s = Replace (s, "COLOR_OFF", "color_off")
        // ^^^ just in case someone makes it upper-case before sending to ProcessText
        game.textprocessorcommandresult = "</span>"
      }
      dictionary add (game.textprocessorcommands, "color_off", scr)
      scr => {
        s = Mid (section, 4)
        s = ProcessTextSection (s, data)
        s = UCase (s)
        game.textprocessorcommandresult = s
      }
      dictionary add (game.textprocessorcommands, "uc", scr)
      scr => {
        s = Mid(section, 4)
        s = LCase(s)
        game.textprocessorcommandresult = s
      }
      dictionary add (game.textprocessorcommands, "lc", scr)
      scr => {
        s = Mid(section, 5)
        s = CapFirst(s)
        game.textprocessorcommandresult = s
      }
      dictionary add (game.textprocessorcommands, "cap", scr)
    ]]></start>
  </game>
  <object name="room">
    <inherit name="editor_room" />
    <isroom />
    <description><![CDATA[{color_on}{lc:THIS IS IN COLOR.}{color_off} BUT THIS IS NOT IN COLOR.<br/><br/>{color_on}{uc:this is in color.}{color_off} but this is not in color.<br/><br/>{color_on}{cap:this} is a test to see if the color will work after a line break.<br/><br/>If this is still in color, it worked. {color_off}The color is now off.]]></description>
    <object name="player">
      <inherit name="editor_object" />
      <inherit name="editor_player" />
    </object>
  </object>
</asl>

Oh, I forgot all about this topic! Oops.

I wasn't aware that I could call ProcessText() at will. This will probably be very useful, though I think it will be more elegant in this situation to use the [Template] notation (which I'd completely forgotten was a thing, though I'm sure it was brought to my attention ages ago.) If [Template] processes before LCase() as attested, then it should work just fine for most foreseeable situations in this library.

Thanks for the feedback!


@KV

I only just noticed, but you have:

  scr => {
    s = Mid(section, 4)
    s = UCase(s)
    game.textprocessorcommandresult = s
  }
 dictionary add (game.textprocessorcommands, "uc", scr)

A user typing {cap:{lamp.alias}} probably wants Magic lamp, not {Lamp.alias} (which then comes out as unparsed code because the object name doesn't match)

Wouldn't it make more sense to put:

      scr => {
        s = Mid (section, 4)
        s = ProcessTextSection (s, data)
        s = UCase (s)
        game.textprocessorcommandresult = s
      }
      dictionary add (game.textprocessorcommands, "uc", scr)

That way, any other text processor commands in the text are run before uppercasing the result. And similar for the other functions. Although I think it would make more sense for the case changing functions to skip HTML, so that {cap:{object:lamp}} will capitalise the first letter of the name rather than the generated <.


I only just noticed, but you have [...] Wouldn't it make more sense to put [...]

My Magic 8-Ball says: Signs point to yes.


scr => {
  s = Mid (section, 4)
  s = ProcessTextSection (s, data)
  s = SafeUCase (s)
  game.textprocessorcommandresult = s
}
dictionary add (game.textprocessorcommands, "uc", scr)
scr => {
  s = Mid (section, 4)
  s = ProcessTextSection (s, data)
  s = SafeLCase (s)
  game.textprocessorcommandresult = s
}
dictionary add (game.textprocessorcommands, "lc", scr)
scr => {
  s = Mid (section, 5)
  s = ProcessTextSection (s, data)
  s = SafeCapFirst (s)
  game.textprocessorcommandresult = s
}
dictionary add (game.textprocessorcommands, "cap", scr)

and…

<function name="SafeUCase" parameters="input" type="string">
  output = ""
  pos = 1
  while (pos <= LengthOf (input)) {
    start = pos
    if (IsRegexMatch ("^.{" + (pos-1) + "}<\\w", input)) {
      pos = FindTagEnd (input, pos)
      if (pos = 0) {
        // tag with no ">" isn't a real tag
        return (output + UCase (Mid (input, start)))
      }
      pos = pos + 1
      output = output + Mid (input, start, pos - start)
    }
    else {
      pos = Instr (pos, input, "<")
      if (pos = 0) {
        // no more "<" in the string
        return (output + UCase (Mid (input, start)))
      }
      output = output + UCase (Mid (input, start, pos))
    }
  }
  return (output)
</function>

<function name="SafeLCase" parameters="input" type="string">
  output = ""
  pos = 1
  while (pos <= LengthOf (input)) {
    start = pos
    while (IsRegexMatch ("^.{" + (pos-1) + "}<\\w", input)) {
      pos = FindTagEnd (input, pos)
      if (pos = 0) {
        // tag with no ">" isn't a real tag
        return (output + LCase (Mid (input, start)))
      }
      pos = pos + 1
      output = output + Mid (input, start, pos - start)
    }
    else {
      pos = Instr (pos, input, "<")
      if (pos = 0) {
        // no more "<" in the string
        return (output + LCase (Mid (input, start)))
      }
      output = output + LCase (Mid (input, start, pos))
    }
  }
  return (output)
</function>

<function name="SafeCapFirst" parameters="s" type="string">
  pos = 1
  while (IsRegexMatch ("^.{" + (pos-1) + "}<\\w", input)) {
    found = FindTagEnd (s, pos)
    if (found = 0) {
      return (Left (s, pos) + UCase (Mid (s, pos+1, 1)) + Mid (s, pos+2))
    }
    else {
      pos = found + 1
    }
  }
  return (Left (s, pos-1) + UCase (Mid (s, pos, 1)) + Mid (s, pos+1))
</function>

<function name="FindTagEnd" parameters="s, pos" type="int">
  // finds the first ">" in a string that is not inside a quoted string
  quote = ""
  if (not IsDefined ("pos")) {
    pos = 1
  }
  for (i, pos, LengthOf (s)) {
    char = Mid (s, i, 1)
    switch (char) {
      case (">") {
        return (i)
      }
      case ("\"", "'") {
        if (quote = "") {
          quote = char
        }
        else if (quote = char) {
          quote = ""
        }
      }
    }
  }
  return (0)
</function>

That version of CapFirst is still a bit wonky… I think in most cases, a naive user might expect CapFirst("\"hello!\" he cried") to give you "Hello!" he cried – capitalising the first non-punctuation character.

But that's a harder task than you might think. Here's a guess:

<function name="IntuitiveCapFirst" parameters="s" type="string">
  regex = "^(?<left>(<\\w([^=\"'>]+|=[^>'\"]|=\"[^\"]\"|='[^']')*>|<(?!\\w|([^=\"'>]+|=[^>'\"]|=\"[^\"]\"|='[^']')*>)|[^\\w\\d<]+)*)(?<right>.*)$"
  if (IsRegexMatch (regex, s, "SplitAtFirstVisibleCharacter")) {
    parts = Populate (regex, s, "SplitAtFirstVisibleCharacter")
    return (DictionaryItem (parts, "left") + CapFirst (DictionaryItem (parts, "left")))
  }
  else {
    // I don't think it should be possible to get here, unless I made a mistake in the regex
    return (CapFirst (s))
  }
</function>

(not tested yet)
This should capitalise the first letter or number that isn't part of an HTML tag. It's more complex than it needs to be because it doesn't just search for the first > after the <, but understands that you can have things like <img alt="Some string that contains a > or <" />


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums