From e51ccad6888ff8da8db1e6c4bc3a053ca1b9062f Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sat, 23 Mar 2024 09:14:26 -0500 Subject: [PATCH 1/5] Add a PGML tag block. The syntax for a tag block is ``` [^ contents ^]{ tag => 'tag name', attributes => { class => 'my-class', ... }, tex_begin => 'tex start code', tex_end => 'tex end code' } ``` or equivalently `[^ contents ^]{'tag name'}{{ class => 'my-class', ... }}{'tex start code'}{'tex end code'}` using the short form for passing options. All of the options are optional (with the tag name defaulting to a 'div'). So you can do `[^ hello ^]`, and that will render a `div` whose contents are "hello". The `attibutes` option should be a reference to a hash containing any HTML attributes you want the tag to have. The contents of a tag are PGML. So you can do ``` [^ [# [. [`x`] .] [. [`y`] .]* [. [^ [`1`] ^]{'span'}{{ style => 'color:blue' }} .] [. [`2`] .]* #] ^] ``` The above example also shows that a tag can be used in the cell of a niceTable. Note that tag blocks can also contain other tag blocks. The `tex_begin` and `tex_end` options are only used when the displayMode is "TeX". The content will be wrapped in the values of those options in that case. For example, ``` [^ My blue equation is [`x + y = 3`] ^]{'div'}{{ style => 'color:blue' }}{'\color{blue}'} ``` Note that when displayMode is "TeX", the contents (including the values of `tex_begin` and `tex_end`) are always wrapped in grouping braces. So you don't have to provide grouping (as would be needed for the color statement in the above example or any content after it would also be blue). Note that the quotes on the tag name, tex_begin, and tex_end options are needed. One of the main reasons for this new PGML block is to provide an easier way for a problem author to specify where the feedback for a MultiAnswer object with singleResult set should go. This is demonstrated in the following example: ``` DOCUMENT(); loadMacros('PGstandard.pl', 'PGML.pl', 'parserMultiAnswer.pl'); $multians = MultiAnswer(1, 4, 9)->with( singleResult => 1, checker => sub { my ($correct, $student, $self, $ans) = @_; my $score = 0; for (0 .. $#$student) { $score += 1 if $correct->[$_] == $student->[$_]; } return $score / @$correct; } ); BEGIN_PGML [^ [# [. [`x`] .] [. [`x^2`] .]*{ headerrow => 1 } [. [`1`] .] [. [_]{$multians} .]* [. [`2`] .] [. [_]{$multians} .]* [. [`3`] .] [. [_]{$multians} .] #]{ valign => 'middle', padding => [ 0.5, 0.5 ] } ^]{'div'}{{ class => 'mx-auto ww-feedback-container ww-fb-align-middle' }} END_PGML ENDDOCUMENT(); ``` Note that currently the contents of a tag block are rendered as if the tag block was not there for the "PTX" displayMode. --- macros/core/PGML.pl | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/macros/core/PGML.pl b/macros/core/PGML.pl index 9a9ad891d4..c24c30d4f7 100644 --- a/macros/core/PGML.pl +++ b/macros/core/PGML.pl @@ -52,8 +52,8 @@ package PGML::Parse; my $emphasis = '\*+|_+'; my $chars = '\\\\.|[{}[\]()\'"]'; my $ansrule = '\[(?:_+|[ox^])\]\*?'; -my $open = '\[(?:[!<%@$#.]|::?:?|``?`?|\|+ ?)'; -my $close = '(?:[!>%@$#.]|::?:?|``?`?| ?\|+)\]'; +my $open = '\[(?:[!<%@$#.^]|::?:?|``?`?|\|+ ?)'; +my $close = '(?:[!>%@$#.^]|::?:?|``?`?| ?\|+)\]'; my $noop = '\[\]'; my $splitPattern = @@ -442,7 +442,7 @@ sub Rule { my $self = shift; my $token = shift; if ($self->{atLineStart}) { -### check for line end or braces + # check for line end or braces $self->Item("rule", $token, { options => [ "width", "height", "size" ] }); $self->{ignoreNL} = 1; } else { @@ -619,6 +619,14 @@ sub NOOP { cellcss texpre texpost texencase rowcolor rowcss headerrow rowtop rowbottom valign rows ) ] }, + "[^" => { + type => 'tag', + parseAll => 1, + allowPar => 1, + isContainer => 1, + terminator => qr/\^\]/, + options => [qw(tag attributes tex_begin tex_end)] + }, "[:" => { type => 'math', parseComments => 1, @@ -1282,6 +1290,7 @@ sub string { /forced/ && do { $string = $self->Forced($item); last }; /comment/ && do { $string = $self->Comment($item); last }; /table/ && do { $string = $self->Table($item); last }; + /tag/ && do { $string = $self->Tag($item); last }; PGML::Warning "Warning: unknown block type '$item->{type}' in " . ref($self) . "::format\n"; } push(@strings, $string) unless (!defined $string || $string eq ''); @@ -1338,6 +1347,11 @@ sub Table { return ($item->{hasStar} ? main::LayoutTable($table, @options) : main::DataTable($table, @options)); } +sub Tag { + my ($self, $item) = @_; + return $self->string($item); +} + sub Math { my $self = shift; my $item = shift; @@ -1645,6 +1659,11 @@ sub Math { return main::general_math_ev3($self->SUPER::Math(@_)); } +sub Tag { + my ($self, $item) = @_; + return main::tag($item->{tag} // 'div', %{ $item->{attributes} // {} }, $self->string($item)); +} + ###################################################################### ###################################################################### @@ -1789,6 +1808,11 @@ sub Math { return main::general_math_ev3($self->SUPER::Math(@_)); } +sub Tag { + my ($self, $item) = @_; + return '{' . ($item->{tex_begin} // '') . $self->string($item) . ($item->{tex_end} // '') . '}'; +} + ###################################################################### ###################################################################### From 039b7e21bceeee674631fa42cd6912d37fc09313 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sat, 23 Mar 2024 22:01:18 -0500 Subject: [PATCH 2/5] Switch to using `[< ... >]` instead of `[^ ... ^]` for tags. Change the general syntax to ``` [< content >]{ html => [ 'tag_name', class => 'my-class', ... ], tex => [ 'tex begin', 'tex end' ], ptx => [ 'tag name', { attribute => 1, ... }, 'separator' ] } ``` or `[< content >]{['tag_name', class => 'my-class', ...]}{['tex begin', 'tex end']}{['tag name', { attribute => 1, ... }, 'separator']}` The `html`, `tex`, and `ptx` options can also just be strings. For `html` and `ptx` that string will be the tag name. If the `tex` argument is a string then the contents will be wrapped in a TeX environment by that name. Note that for `html` when the argument is given as an array, the tag name can also be omitted. In that case a `div` tag will be used. So you can do `[< content >]{[ class => 'my-class' ]}`. A link can be created with this as well. For example, `[]{[ 'a', href => 'https://www.google.com' ]}`. I would be willing to switch the default to being an `a` tag. So then to get a `div` you would need `[< content >]{[ 'div', class => 'my-class' ]}`, but to obtain a link you could do `[]{[ href => 'https://www.google.com' ]}`. That would make the `[< ... >]` block more like the originaly intended link. I left the default as a `div` because that usage is probably nicer for what I would use this for (I never put links in problems). For now the only allowed HTML tags are `div`, `span`, and `a`. What other tags should be allowed? Note that for `PTX` display mode this uses the `NiceTables::tag` method. I am not sure if that was a good idea, but it was an idea. @Alex-Jordan: Please advise. --- macros/core/PGML.pl | 51 ++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/macros/core/PGML.pl b/macros/core/PGML.pl index c24c30d4f7..feacd08a77 100644 --- a/macros/core/PGML.pl +++ b/macros/core/PGML.pl @@ -52,8 +52,8 @@ package PGML::Parse; my $emphasis = '\*+|_+'; my $chars = '\\\\.|[{}[\]()\'"]'; my $ansrule = '\[(?:_+|[ox^])\]\*?'; -my $open = '\[(?:[!<%@$#.^]|::?:?|``?`?|\|+ ?)'; -my $close = '(?:[!>%@$#.^]|::?:?|``?`?| ?\|+)\]'; +my $open = '\[(?:[!<%@$#.]|::?:?|``?`?|\|+ ?)'; +my $close = '(?:[!>%@$#.]|::?:?|``?`?| ?\|+)\]'; my $noop = '\[\]'; my $splitPattern = @@ -619,14 +619,6 @@ sub NOOP { cellcss texpre texpost texencase rowcolor rowcss headerrow rowtop rowbottom valign rows ) ] }, - "[^" => { - type => 'tag', - parseAll => 1, - allowPar => 1, - isContainer => 1, - terminator => qr/\^\]/, - options => [qw(tag attributes tex_begin tex_end)] - }, "[:" => { type => 'math', parseComments => 1, @@ -698,13 +690,12 @@ sub NOOP { options => [ "source", "width", "height", "image_options" ] }, "[<" => { - type => 'link', - parseComments => 1, - parseSubstitutions => 1, - terminator => qr/>\]/, - terminateMethod => 'terminateGetString', - cancelNL => 1, - options => [ "text", "title" ] + type => 'tag', + parseAll => 1, + allowPar => 1, + isContainer => 1, + terminator => qr/>\]/, + options => [qw(html tex ptx)] }, "[%" => { type => 'comment', parseComments => 1, terminator => qr/%\]/, allowPar => 1 }, "[\@" => { @@ -1661,7 +1652,14 @@ sub Math { sub Tag { my ($self, $item) = @_; - return main::tag($item->{tag} // 'div', %{ $item->{attributes} // {} }, $self->string($item)); + my %whitelist = (a => 1, div => 1, span => 1); + my @attributes = ref($item->{html}) eq 'ARRAY' ? @{ $item->{html} } : $item->{html}; + my $tag = @attributes % 2 ? (shift @attributes // 'div') : 'div'; + unless ($whitelist{$tag}) { + PGML::Warning qq{The tag "$tag" is not allowed}; + return $self->string($item); + } + return main::tag($tag, @attributes, $self->string($item)); } ###################################################################### @@ -1810,7 +1808,13 @@ sub Math { sub Tag { my ($self, $item) = @_; - return '{' . ($item->{tex_begin} // '') . $self->string($item) . ($item->{tex_end} // '') . '}'; + my ($tex_begin, $tex_end); + if (ref($item->{tex}) eq 'ARRAY') { + ($tex_begin, $tex_end) = @{ $item->{tex} }; + } elsif ($item->{tex}) { + ($tex_begin, $tex_end) = ("\\begin{$item->{tex}}", "\\end{$item->{tex}}"); + } + return '{' . ($tex_begin // '') . $self->string($item) . ($tex_end // '') . '}'; } ###################################################################### @@ -1960,6 +1964,15 @@ sub Math { return main::general_math_ev3($self->SUPER::Math(@_)); } +sub Tag { + my ($self, $item) = @_; + my @args = ref($item->{ptx}) eq 'ARRAY' ? @{ $item->{ptx} } : $item->{ptx}; + if (my $tag = shift @args) { + return NiceTables::tag($self->string($item), $tag, @args); + } + return $self->string($item); +} + ###################################################################### ###################################################################### From f81cbf1220ab5925971e0cf45e6de4a1bfea23a3 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Thu, 28 Mar 2024 21:02:44 -0500 Subject: [PATCH 3/5] Remove the `a` tag from the whitelist for the new tag block. --- macros/core/PGML.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/core/PGML.pl b/macros/core/PGML.pl index feacd08a77..13cd004093 100644 --- a/macros/core/PGML.pl +++ b/macros/core/PGML.pl @@ -1652,7 +1652,7 @@ sub Math { sub Tag { my ($self, $item) = @_; - my %whitelist = (a => 1, div => 1, span => 1); + my %whitelist = (div => 1, span => 1); my @attributes = ref($item->{html}) eq 'ARRAY' ? @{ $item->{html} } : $item->{html}; my $tag = @attributes % 2 ? (shift @attributes // 'div') : 'div'; unless ($whitelist{$tag}) { From ac6f9b2ef6e45b817e1380d6fd0f38c130f9a2f8 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Thu, 28 Mar 2024 22:27:12 -0500 Subject: [PATCH 4/5] Remove the `allowPar` option for the tag block. This prevents new lines from working correctly inside a div tag. --- macros/core/PGML.pl | 1 - 1 file changed, 1 deletion(-) diff --git a/macros/core/PGML.pl b/macros/core/PGML.pl index 13cd004093..86b3b141b5 100644 --- a/macros/core/PGML.pl +++ b/macros/core/PGML.pl @@ -692,7 +692,6 @@ sub NOOP { "[<" => { type => 'tag', parseAll => 1, - allowPar => 1, isContainer => 1, terminator => qr/>\]/, options => [qw(html tex ptx)] From ee33ad22318787ff0fa53e80cfc33d5e3bc64c43 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sat, 30 Mar 2024 15:26:41 -0500 Subject: [PATCH 5/5] Perform a bit of html validation for PGML tag blocks that are spans. A span tag block is not allowed to contain any PGML blocks that have the type indent, align, par, list, bullet, answer, heading, rule, code, pre, verbatim, table, or tag. If a span tag block is detected with any of those things in it, a warning is issued and the contents rendered directly without the span tag. This helps to prevent problem authors from doing the wrong thing. It is valid for a span to contain a span, but if a span tag block contains another tag bloc, the outer span tag block does not have the information to determine what the inner tag block is (span or div). Since I don't think it is particularly useful to have a span within a span, I just blocked any tag block in a span. --- macros/core/PGML.pl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/macros/core/PGML.pl b/macros/core/PGML.pl index 86b3b141b5..b2b9ae5888 100644 --- a/macros/core/PGML.pl +++ b/macros/core/PGML.pl @@ -1658,6 +1658,15 @@ sub Tag { PGML::Warning qq{The tag "$tag" is not allowed}; return $self->string($item); } + if ($tag eq 'span') { + for my $subblock (@{ $item->{stack} }) { + if ($subblock->{type} =~ /^(indent|align|par|list|bullet|answer|heading|rule|code|pre|verbatim|table|tag)$/) + { + PGML::Warning qq{A "span" tag may not contain a $subblock->{type}}; + return $self->string($item); + } + } + } return main::tag($tag, @attributes, $self->string($item)); }