Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typst property output #9648

Merged
merged 4 commits into from
Apr 15, 2024
Merged

Typst property output #9648

merged 4 commits into from
Apr 15, 2024

Conversation

gordonwoodhull
Copy link
Contributor

@gordonwoodhull gordonwoodhull commented Apr 10, 2024

Here is my work in progress on Typst property output. Supercedes #9623.

https://github.com/gordonwoodhull/pandoc/tree/typst-property-output

It handles 8 cases of attribute-bearing AST nodes:

  • span text (no element properties because text() is the closest element to span)
  • div and div text
  • table and table text
  • td and td text (3 cases)

Now that I have tests I would be comfortable implementing the rest, but I haven't needed them yet. Thought I'd check in to ask if I'm on track here. All suggestions are welcome!

I think the code is more idiomatic Haskell (but not perfect) and the usage is terse (except for the align case, see below). Some details of whitespace/line breaks probably need more work.

There aren't row elements in Typst; tr attributes would have to be underlaid on cell properties.

align is messy

There is one case that is ugly - the Typst reader extracts (horizontal) align, but this needs to be coalesced with vertical-align for the Typst cell align. An alternative design would be to not remove the style in the reader, and allow Lua to override the whole property. Technically I should also parse typst:align as addition and then dedupe, because Typst will error if there are duplicate terms in the align expression, but Lua only sees vertical-align currently, so it works out.


formatTypstProps :: [(Text, Text)] -> [Text]
formatTypstProps =
map (\(k,v) -> last (T.splitOn (T.pack ":") k) <> ": " <> v)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid last, as it's a partial function.
Also you don't need T.pack ":"; ":" will work by itself because we have OverloadedStrings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points. I had to improve validation to drop keys like typst:foo:bar, so I took the opportunity to pull the last element at input time and not worry about invalid input during output.

Comment on lines 43 to 178
, " )"
]
, "table text" =:
let headers = []
rows = []
in tableWith ("", [], [("typst:text:fill", "orange")])
emptyCaption
[]
(TableHead nullAttr $ toHeaderRow headers)
[TableBody nullAttr 0 [] $ map toRow rows]
(TableFoot nullAttr [])
=?> unlines [
"#figure("
, " align(center)[#text(fill: orange)[#table("
, " columns: 0,"
, " align: (),"
, " )]]"
, " , kind: table"
, " )"
]
, "table cell" =:
let headers = []
rows = [Row nullAttr [
cellWith ("", [], [("typst:fill", "blue")]) AlignDefault (RowSpan 1) (ColSpan 1) $ para "Foo"
, simpleCell $ para "Bar"
]]
in table
emptyCaption
[(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault)]
(TableHead nullAttr $ toHeaderRow headers)
[TableBody nullAttr 0 [] rows]
(TableFoot nullAttr [])
=?> unlines [
"#figure("
, " align(center)[#table("
, " columns: 2,"
, " align: (auto,auto,),"
, " table.cell(fill: blue)[Foo"
, ""
, " ], [Bar"
, ""
, " ],"
, " )]"
, " , kind: table"
, " )"
]
, "table cell text no attr" =:
let headers = []
rows = [Row nullAttr [
cellWith ("", [], [("typst:text:fill", "orange")]) AlignDefault (RowSpan 1) (ColSpan 1) $ para "Foo"
, simpleCell $ para "Bar"
]]
in table
emptyCaption
[(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault)]
(TableHead nullAttr $ toHeaderRow headers)
[TableBody nullAttr 0 [] rows]
(TableFoot nullAttr [])
=?> unlines [
"#figure("
, " align(center)[#table("
, " columns: 2,"
, " align: (auto,auto,),"
, " [#set text(fill: orange); Foo"
, ""
, " ], [Bar"
, ""
, " ],"
, " )]"
, " , kind: table"
, " )"
]
, "table cell text w attr" =:
let headers = []
rows = [Row nullAttr [
cellWith ("", [], [("typst:text:fill", "orange")]) AlignCenter (RowSpan 1) (ColSpan 1) $ para "Foo"
, simpleCell $ para "Bar"
]]
in table
emptyCaption
[(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault)]
(TableHead nullAttr $ toHeaderRow headers)
[TableBody nullAttr 0 [] rows]
(TableFoot nullAttr [])
=?> unlines [
"#figure("
, " align(center)[#table("
, " columns: 2,"
, " align: (auto,auto,),"
, " table.cell(align: center)[#set text(fill: orange); Foo"
, ""
, " ], [Bar"
, ""
, " ],"
, " )]"
, " , kind: table"
, " )"
]
]
]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest just using a "command test" (see test/commands/*.md).
They are easier to create and much easier to maintain (since we can just do make test TESTARGS=--accept to update the expected output instead of doing it manually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, much simpler to write tests that way!

I threw in negative cases: elements that don't have typst:* attributes should not affect the output. This was covered by previous tests but worth it to be somewhat complete.

Comment on lines 115 to 106
-- there is probably a more idiomatic way to do this
ifNotEmpty :: ([a2] -> Doc Text) -> [a2] -> Doc Text
ifNotEmpty f list =
case list of
[] -> ""
_ -> f list

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more elegant to use a fold. E.g. foldl' addTypstProperty mempty props.
If the list props is empty, you get mempty, so you wouldn't need this ugly function.
It should be possible to make this code simpler.

Copy link
Contributor Author

@gordonwoodhull gordonwoodhull Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little bit more complicated, because we have this conditional text we want to add around the lists, like we want to emit either #set text(prop:value); or the empty string at the top of a block where we are applying typst:text:* attributes. We don't want to emit a useless #set text(); (if that's even valid).

And likewise everywhere else: if a block has no attributes, we want to emit #block[content] and not #block()[content], etc.

@cscheid and I looked at it together and agreed that most clear thing to do is not to try to abstract it, but just write functions that both check for the empty list and generate text. This section is about half as many lines of code now.

@gordonwoodhull gordonwoodhull force-pushed the typst-property-output branch from 38d0a75 to c6a21ca Compare April 12, 2024 04:27
@gordonwoodhull gordonwoodhull marked this pull request as ready for review April 12, 2024 04:32
@gordonwoodhull
Copy link
Contributor Author

Okay, I think I have addressed your concerns, as well as adding solid validation of typst:* keys.

Glad to make any other changes! Should I add documentation somewhere?

I think I'd prefer to wait to implement output of properties from other elements until we get more experience, unless you have any requests.

<span>, <div>, <table> and <td> are the most important elements as far as Quarto is concerned.

I can do another PR later with other elements as our users inevitably test the boundaries of this, or we find more uses for Typst properties.

Thanks for all your helpful comments so far.

@jgm
Copy link
Owner

jgm commented Apr 12, 2024

As for documentation, one possibility would be to have a separate document along the lines of
https://pandoc.org/org.html
whose source is doc/org.md. Alternatively it could be a section in the manual like the current section for EPUBs, but my thinking is that this concerns a pretty niche feature that is only accessible through filters, so it probably is best to document it separately...

Comment on lines 97 to 99
if length(spl) < 2 || length(spl) > 3
|| head spl /= "typst"
|| (length(spl) == 3 && spl !! 1 /= "text")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We like to avoid using partial functions like head. I think it's cleaner to use pattern matching for this kind of thing. Could you do something like

case T.splitOn ":" k of
  "typst":"text":x -> Right (x,v)
  "typst":x -> Left (x, v)
  _ -> Nothing

Second thought: instead of using partitionEithers . catMaybes . map, why not just use a fold that directly constructs the tuple? This way you also avoid needing to use Maybe or Either.

pickTypstAttrs = foldr go ([],[])
  where
    go (k,v) (eltProps, textProps) =
      case T.splitOn ":" k of
        "typst":"text":x -> (eltProps, (x,v):textProps)
        "typst":x -> ((x,v):eltProps, textProps)
        _ -> (eltProps, textProps)

Or even more compactly with Data.Bifunctor.(first,second)

pickTypstAttrs = foldr go ([],[])
  where
    go (k,v)
      case T.splitOn ":" k of
        "typst":"text":x -> second ((x,v):)
        "typst":x -> first ((x,v):)
        _ -> id

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very nice!

All five of my logical conditions covered by three patterns, and the meaning is very clear with the bifunctor approach.

Comment on lines 105 to 106
formatTypstProps :: [(Text, Text)] -> [Text]
formatTypstProps =
map (\(k,v) -> k <> ": " <> v)

toTypstPropsListSep :: [(Text, Text)] -> Doc Text
toTypstPropsListSep typstAttrs =
literal (T.intercalate ", " $ formatTypstProps typstAttrs)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this approach the attribute lists will be unbreakable.
That's probably not what we want. Better:

formatTypstProps = map (\(k,v) -> literal (k <> ": " <> v))
toTypstPropsListSep = hsep . intersperse "," . formatTypstProps

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, knew I was missing something about line breaks. Thanks!


toTypstTextElement :: [(Text, Text)] -> Doc Text -> Doc Text
toTypstTextElement [] content = content
toTypstTextElement typstTextAttrs content = "#text" <> parens (toTypstPropsListSep typstTextAttrs) <> brackets content
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could simplify by using toTypstPropsListParens here instead of the spelled out version

@gordonwoodhull
Copy link
Contributor Author

gordonwoodhull commented Apr 12, 2024

Excellent feedback - I think it is very clean and simple now.

I've pushed a first draft of the docs. The wording is a little rough but I think they are complete.

Will take another pass over the weekend and clean up.

@iandol
Copy link
Contributor

iandol commented Apr 13, 2024

Amazingly useful contribution @gordonwoodhull!!! 🥳

the command

```sh
quarto pandoc -f html -t typst --lua-filter ./typst-property-example.lua
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is pandoc's documentation, not quarto, shouldn't this just be pandoc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep!

This implements output of Typst properties from attributes of the
forms

* `typst:prop` - the value should be interpreted as raw Typst code
to set a parameter on the corresponding Typst element

* `typst:text:prop` - the value should be interpreted as raw Typst
code to set a parameter on a Typst `#set text()` rule, or text
element as appropriate.

This commit covers the following elements:
* span text
* div and div text
* table and table text
* td and td text

It would be possible to support more elements in the future.
@gordonwoodhull gordonwoodhull force-pushed the typst-property-output branch from cfb080c to 62b7cec Compare April 13, 2024 23:26
@gordonwoodhull
Copy link
Contributor Author

I fixed the example, did a few more copy edits, and squashed everything into one commit.

I think this is ready! Please let me know if you want any further changes.

```

```
% pandoc -f html -t typst
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a difference between this test and the one at line 76?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to have a test of an attribute on table?

Copy link
Contributor Author

@gordonwoodhull gordonwoodhull Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess I added negative tests for both table and cell without somehow realizing they are the same. Removed the redundant test.

this is the annoying case where we have to concatenate align
and i forgot to update it when cleaning up attr validation
Because it's already inside figure & align,
so these settings won't pollute anything
@gordonwoodhull
Copy link
Contributor Author

I found that I had failed to update the ugly special case for align when I improved validation, so I pushed a fix and associated test.

Also I noticed that we can use a set-text rule instead of text element for table, since the table is inside the figure & align scopes and the set-text rule won't leak out.

@jgm jgm merged commit 99a4591 into jgm:main Apr 15, 2024
8 of 12 checks passed
@jgm
Copy link
Owner

jgm commented Apr 15, 2024

I think it's good to go! Thanks.

@gordonwoodhull gordonwoodhull deleted the typst-property-output branch April 15, 2024 17:05
@gordonwoodhull
Copy link
Contributor Author

Thank you!

@gordonwoodhull
Copy link
Contributor Author

This is working great and we'd like to include "Typst CSS" in Quarto 1.5.

When you're around and have a minute, please release 3.1.14!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants