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

A simpler way to input tables and grids content #4071

Open
RaulDurand opened this issue May 5, 2024 · 19 comments
Open

A simpler way to input tables and grids content #4071

RaulDurand opened this issue May 5, 2024 · 19 comments
Labels
feature request New feature or request syntax About syntax, parsing, etc.

Comments

@RaulDurand
Copy link

RaulDurand commented May 5, 2024

Description

FR: A simpler way to input cells in tables and grids

Current Issues:
Bracketed cell entries: The current requirement to use brackets for each cell is cumbersome and often confusing, particularly in large tables.

No explicit row separator: Without a specific row separator, cells are placed based on the previously defined number of columns, making it difficult to accurately differentiate between rows. This issue becomes particularly problematic when cells are removed or replaced, often resulting in a disorganized table structure during editing. As a result, quick compilation loses its effectiveness because cells shift to different positions, causing confusion during the editing process.
Furthermore, when rows have empty cells at the end, multiple empty brackets are required, adding unnecessary clutter. This issue could be resolved with a dedicated newline command, simplifying table input.

Proposed solution:

One content field with cell separator: Implement a single content field where the ampersand (&) symbol acts as a separator between cells. This minimizes the need for multiple brackets and commas. The & symbol would hold special significance, similar to this and other symbols used for specific formatting purposes: - for itemization, + for enumerators, \ for lists, and & for equation alignment.

Row Separator: Utilize the backslash \ as a row separator to mark the start of a new row. This symbol would have a dedicated meaning, much like it does in lists where each item is separated by a backslash.

Use Case

Consider the following example, which simplifies the input format:

Currently:

#table(
  stroke: none,  columns: (auto, 1fr, 1fr),
  [09:45], [Opening Keynote], [free],
  [10:30], [Talk: Typst's Future], [free],
  [_Lunch break_], [  ], [  ],
  table.hline(start: 1),
  [16:00], [Workshop: Tables], [needs payment],
  table.hline(),
  [18:00],  [_Dinner_], [  ], [  ],
)

Proposed:

#table(
  stroke: none,  columns: (auto, 1fr, 1fr), [
    09:45  &  Opening Keynote  &  free \
    10:30  &  Talk: Typst's Future  &  free \
    _Lunch break_  \
    #table.hline(start: 1)
    16:00  &  Workshop: Tables  &  needs payment \
    #table.hline()
    18:00  &  _Dinner_ 
  ]
)

The proposed format seeks to streamline the input process, making table and grid creation more intuitive and less error-prone. Additionally, its resemblance to LaTeX could help make the transition smoother for newcomers, providing a familiar structure and easing the learning curve. Furthermore, the simplicity of the proposed format aligns more closely to a markdown syntax.

@RaulDurand RaulDurand added the feature request New feature or request label May 5, 2024
@notaduck448
Copy link

This feels way too different from the rest of Typst's syntax and the '&' having a special meaning outside of math mode would create a lot of problems. You would have to give it a special meaning only when used inside a content block passed to the table function.

@istudyatuni
Copy link
Contributor

I think it would be better to add table.row, and use it for making each row separately, like

#table(
  // ...
  table.row[][],
  // ...
)

@RaulDurand
Copy link
Author

This feels way too different from the rest of Typst's syntax and the '&' having a special meaning outside of math mode would create a lot of problems. You would have to give it a special meaning only when used inside a content block passed to the table function.

Alright, maybe another separator could do the job.

@laurmaedje laurmaedje changed the title FR: A simpler way to input tables and grids content A simpler way to input tables and grids content May 7, 2024
@laurmaedje laurmaedje added the syntax About syntax, parsing, etc. label May 7, 2024
@Enivex
Copy link
Collaborator

Enivex commented May 7, 2024

I don't think a future syntax sugar for simple tables is necessarily a bad idea, but it would require a lot of consideration.

@Doublonmousse
Copy link

table.row seems to me like a syntax that wouldn't feel foreign inside typst.

The thing to look for though is how well that would work with colspan and rowspan (cells that spans multiple rows or columns). Not sure that wouldn't break things short of having a separate simple table mode.

@RaulDurand
Copy link
Author

This feels way too different from the rest of Typst's syntax and the '&' having a special meaning outside of math mode would create a lot of problems. You would have to give it a special meaning only when used inside a content block passed to the table function.

Alright, maybe another separator could do the job.

The cell separator could be a constant, e.g. #sep, that is non-op (or error) outside a table/grid, or other available symbol or even a two-symbol operator.

@PgBiel
Copy link
Contributor

PgBiel commented May 8, 2024

I think it would be better to add table.row, and use it for making each row separately, like

#table(
  // ...
  table.row[][],
  // ...
)

I'd be ok with either this or a rowbreak() element (or both). In other words, a way to ensure that you always end a row at a certain point.

With that said, I'm not sure alignment points in markup mode would be used for tables, but rather perhaps for actual alignment points if the layout system is ever made flexible enough for that.

@RaulDurand
Copy link
Author

RaulDurand commented May 8, 2024

why \ would not be good enough to end a row? If a newline is needed inside a cell, which is not very frequent, a #block or a #table.cell instance could be used. Note that in #terms, \ is used to end a term.

Edit: I think I got it. rowbreak() would be used in the current syntax.

@Coekjan
Copy link
Contributor

Coekjan commented May 8, 2024

At least now (v0.11.0), I don't think your proposed style really simplifies the syntax. And actually, in my view, your style is not consistent with typst syntax style.

If let me choose between them, I would like the current table syntax absolutely.

@RaulDurand
Copy link
Author

RaulDurand commented May 8, 2024

At least now (v0.11.0), I don't think your proposed style really simplifies the syntax. And actually, in my view, your style is not consistent with typst syntax style.

Thank you, @Coekjan, for sharing your perspective. I'd like to present my thoughts and respectfully express my disagreement.

The first line of Typst's description on the GitHub page reads, "Typst is a new markup-based typesetting system," followed by, "Built-in markup for the most common formatting tasks". Thus, once most commands are set up in the beginning of a document, we can easily use markup for titles, lists, terms, bold text, etc, ensuring clear content.

In my opinion, the current syntax for tables and grids contradicts this philosophy. While matrices and vectors employ a form of markup, tables and grids lack a similar approach that would ensure clarity and conciseness. Instead, they rely on a plethora of braces and lack the ability to declare new rows.

@RaulDurand
Copy link
Author

This feels way too different from the rest of Typst's syntax and the '&' having a special meaning outside of math mode would create a lot of problems. You would have to give it a special meaning only when used inside a content block passed to the table function.

Please refer to my previous post. I believe that using markup aligns well with Typst's syntax. Additionally, it seems that the current table and grid commands are the only ones that require numerous content arguments.

@Coekjan
Copy link
Contributor

Coekjan commented May 9, 2024

Built-in markup for the most common formatting tasks

Different from heading, list, bold-text, in my view, table is a complex thing. Features like cell-merge (colspan/rowspan), cell-alignment and cell-rotation seem difficult to be expressed in markup. This is, I believe, one of the reasons why typst intro the table function along with cell. If we just want to build a simple enough table (markdown-style) in markup-style, I think using tablem could be a good choice. And compiler-builtin support is unnecessary.

lack the ability to declare new rows.

Yes, I believe typst team will consider to add some support to declare a new row. I think it is possible to support declaring new rows within current table function system, and it is unnecessary to intro another (simple) way to do that.

@RaulDurand
Copy link
Author

RaulDurand commented May 9, 2024

Different from heading, list, bold-text, in my view, table is a complex thing. Features like cell-merge (colspan/rowspan), cell-alignment and cell-rotation seem difficult to be expressed in markup. This is, I believe, one of the reasons why typst intro the table function along with cell. If we just want to build a simple enough table (markdown-style) in markup-style, I think using tablem could be a good choice. And compiler-builtin support is unnecessary.

Thank you @Coekjan for your clarifications.
I believe cell-merge (colspan/rowspan) can and must coexists with a possible markup syntax, e.g.,

    [ Lorem ipsum & #table.cell(colspan...) & lorem ipsum \
   ...

(a different separator can be used)

While I believe tablem could be helpful for some users, it's not suitable for most use cases. Furthermore, I think that the table syntax in Typst should aim to be clearer than its LaTeX counterpart, not the other way around.

@gabfssilva
Copy link

Tried to borrow some ideas from Kotlin's type-safe builder and I came up with this:

#table-dsl(inset: 5pt, column => {
  column("id", row => {
    row("1")
    row("2")
    row("3")
  })
  
  column("name", row => {
    row("ana")
    row("marcus")
    row("joana")
  })

  column("age", row => {
    row(18)
    row(32)
    row(26)
  })
})

The code behind it isn't that fancy, but I couldn't make it work without a state variable. Anyway, this is working:

#let table-dsl(inset: 10pt, builder) = {
  let table-state = state("table", ())

  let row-builders = (column) => (builder) => {
    table-state.update(table => {
      let pos = table.position(c => c.name == column)
      let col = table.at(pos)
      col.rows.push(builder)
      table.at(pos) = col
      table
    })
  }

  let columns = (name, column-builder) => {
    table-state.update(table => {
      table.push((name: name, rows: ()))
      table
    })
    
    column-builder(row-builders(name))    
  }

  builder(columns)

  context {
    let t = table-state.get()
    
    let row-size = t.map(v => v.rows.len()).sorted().last()

    let values = ()

    for row-index in range(0, row-size) {
      for column in t {
        values.push(column.rows.at(row-index, default: []))
      }
    }

    let headers = t.map(c => c.name)

    table-state.update(s => ())

    table(
      columns: headers.len(),
      inset: inset,
      align: horizon,
        table.header(..headers),
        ..values.map(r => [#r])
      )
  }
}

Anyway, I have no problem with the current table, so, just tried to provide some different ideas. 😅

@eduardz1
Copy link

Built-in markup for the most common formatting tasks

Different from heading, list, bold-text, in my view, table is a complex thing. Features like cell-merge (colspan/rowspan), cell-alignment and cell-rotation seem difficult to be expressed in markup. This is, I believe, one of the reasons why typst intro the table function along with cell. If we just want to build a simple enough table (markdown-style) in markup-style, I think using tablem could be a good choice. And compiler-builtin support is unnecessary.

lack the ability to declare new rows.

Yes, I believe typst team will consider to add some support to declare a new row. I think it is possible to support declaring new rows within current table function system, and it is unnecessary to intro another (simple) way to do that.

I also believe that to target Typst as a markup-based typesetting language, tables in markdown syntax, like tablem does, should be the default, without having to use external packages.

In the same way that heading() is used when you need more customization, table() should be used when you need more customization compared to the basic markdown syntax. The thing about markdown syntax is that it's very readable even from raw text.

This lowers the barrier of entry of people that are already familiar with markdown significantly, writing tables is a fairly common task.

@RaulDurand
Copy link
Author

In the same way that heading() is used when you need more customization, table() should be used when you need more customization compared to the basic markdown syntax. The thing about markdown syntax is that it's very readable even from raw text.

This lowers the barrier of entry of people that are already familiar with markdown significantly, writing tables is a fairly common task.

I am absolutely agree.

@PgBiel
Copy link
Contributor

PgBiel commented May 23, 2024

Tried to borrow some ideas from Kotlin's type-safe builder and I came up with this:

-- snip --

The code behind it isn't that fancy, but I couldn't make it work without a state variable.

Here's a way to do this purely in code (without relying on context or state) through Typst's join mechanism:

Code

// MIT-0 licensed (feel free to use!)

#let table-dsl(col-callback, ..args) = {
  assert(args.pos().len() == 0, message: "'table-dsl' does not take additional positional arguments, only the 'columns => ...' callback")

  let make-row(cell) = ([#cell],)  // simply wrap in an array
  let make-column(header-cell, row-callback) = {
    // row() row() row() will join the cells
    // into a single array, which we flatten here
    // into a single array of column cells.
    // We wrap that array into a larger array
    // in order to produce an array of column arrays
    // when joining column() column().
    ((header-cell, ..row-callback(make-row)),)
  }
  // array of columns
  // each column is an array of cells
  let columns = col-callback(make-column)
  // transpose into array of rows
  // due to row-major order
  let rows = array.zip(..columns)
  let header = table.header(..rows.remove(0))
  // flatten the rest into a single array of cells
  let cells = rows.join()

  table(columns: columns.len(), ..args.named(), header, ..cells)
}

// Example
#table-dsl(inset: 5pt, column => {
  column("id", row => {
    row("1")
    row("2")
    row("3")
  })
  
  column("name", row => {
    row("ana")
    row("marcus")
    row("joana")
  })

  column("age", row => {
    row(18)
    row(32)
    row(26)
  })
})

resulting table

@PgBiel
Copy link
Contributor

PgBiel commented May 23, 2024

Regarding a dedicated syntax for tables:

Personally, I think Markdown tables in particular are a bad fit for Typst, because they may require a good amount of fiddling to get right. In particular, since you also input the table's lines, you need to use the correct amount of -s in your table for it to look at least somewhat good in your source code, and editing any cells in the table becomes a pain because you have to change the amount of -s, and/or whitespace near them, in all affected lines. Depending on screen size, you may have to limit the amount of dashes as well, which might not be possible if your table is large enough. It's also theoretically possible to make the compiler accept any amount of -s or other such characters regardless, or even ignore whitespace characters between dashes, but then the source code can become pretty ugly, which would undermine what I see to be the only perceived benefit of using such a syntax. (Not to mention that pointing to errors in malformed tables would be hard to get right, in a way that users wouldn't get disappointed with unhelpful errors.)

While I can see how a dedicated syntax for tables could make sense, it would have to account for large tables and be easy to use and modify. The current syntax achieves this well because you can split a function call across multiple lines, and you can lay out the parameters visually in any way you want. With that said, it is also possible to lay out the parameters in a confusing manner, but never in a way that you can't "recover" (follow the parentheses and check the amount of columns). Additionally, editing the contents of a cell is not a problem at all, since each cell is delimited by commas with any amount of whitespace inbetween.
(This doesn't mean the current syntax is perfect - as noted, it is still a bit annoying to change the amount of columns. Though, probably less annoying than a markdown table, since at least you don't have to reconstruct the table's lines, only reformat its cells. LaTeX-like syntax can suffer from similar problems, though to a lesser extent due to the usage of \ to explicitly separate rows.)

Regardless, I think packages are a great way to experiment with other possible approaches. tablem, with support for markdown-like syntax, has been brought up; additionally, @gabfssilva's DSL inspired by Kotlin could become a package of its own (might require some adjustments to be more flexible though, but doesn't seem like a bad idea to me).

On our side, at the compiler, we could consider adding support for an optional table.row construct to let you explicitly separate your rows. However, imo we should only add such a feature if we can find some other reason for it to be there (e.g. add some per-row styling through table.row as well), because this feature, by itself, is already possible today with a bit of code (in particular, you can create a row function which maps each cell parameter to a table.cell(y: row number) where the y is increased with each row call, so this could be a package too!).

Either way, I'm interested to see other suggestions for possible syntaxes, as I'm sure the discussion could benefit from some more diversity 😄

@gabfssilva
Copy link

Here's a way to do this purely in code (without relying on context or state) through Typst's join mechanism:

That's really cool! Based on your example, I guess I was able to improve it further, I'm just not so sure about the possible limitations tho:

Basically, I was able to remove the callbacks, which I think makes it a bit easier:
#let row(cell) = ([#cell],)
#let column(name, rows) = ((name, ..rows),)

#let table-dsl(all-rows, ..args) = {
  assert(
    args.pos().len() == 0, 
    message: "'table-dsl' does not take additional positional arguments"
  )

  let rows = array.zip(..all-rows)
  let header = table.header(..rows.remove(0))
  let cells = rows.join()

  table(columns: header.children.len(), ..args.named(), header, ..cells)
}

#table-dsl(inset: 5pt, {
  column("id", {
    row("1")
    row("2")
    row("3")
  })
  
  column("name", {
    row("ana")
    row("marcus")
    row("joana")
  })

  column("age", {
    row(18)
    row(32)
    row(26)
  })
})

I'll try it in some projects, if I don't find any issues, I'll submit a new package. Thanks for the tip. 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature or request syntax About syntax, parsing, etc.
Projects
None yet
Development

No branches or pull requests

10 participants