How to generate a multilevel list from a data structure?

Is there a method to create a multilevel list from a data structure, e.g. a JSON file?

If I got this JSON file

{
	"information": [
		"First point",
		"Second point",
		[
			"First subpoint"
		],
		"Third point",
		[
			"Second subpoint",
			[
				"First subsubpoint"
			],
		]
	]
}

I want it to be formated like this

  • First point
  • Second point
    • First subpoint
  • Third point
    • Second subpoint
      • First subsubpoint

The closest I got was with this

#let list_recursive(data, level: 0pt) = {
	for item in data {
		if type(item) == array {
			list_recursive(item, level: level+10pt)
		} else {
			list(item, indent: level)
		}
	}
}

#let data = json("file.json")
#list_recursive(data.information)

but then all bullet points are separate lists with a single first level bullet point.

Apparently there is a hidden depth field, which is not accessible to users: Add level field to bullet list items for easier format and style minipulation · Issue #4520 · typst/typst · GitHub

1 Like

TMI below, sorry. This is a fun puzzle (for my simple mind at least) and a worthwhile question. There’s a few things upfront that could be hindering your approach: (1) the elements of lists are items (accessed with list.item), so if you want just one list, that will be the way to go. (2) Making a single list with indentations requires nesting of items, and doing so will take care of indentation automatically.

One issue that I didn’t see coming when thinking about this comes from the structure of lists in Typst. For example, this code…

#repr[
- First point
- Second point
    - First subpoint
]

… shows the underlying structure of lists:

sequence(
  [ ],
  item(body: [First point]), 
  [ ],
  item(body: sequence(
    [Second point],
    [ ], 
    item(body: [First subpoint])
  ),),
  [ ],
)

As seen here, when a list item is followed by an increase in indentation (aka a sublist), its item object is the parent to the sublist as well (see [Second point]). Therefore, in order to code this correctly, one needs to check if the item in the json data structure is followed by an array. This means that simple element-wise iteration with a recursive function is not an option here.

To work around this, you might be interested in using only objects, not arrays, in your json file, like this:

{
	"information": {
		"First point": null,
		"Second point": {"First subpoint": null},
		"Third point": {"Second subpoint": {"First subsubpoint": null}}
}

… then pure recursion could be used. But this format is somewhat ugly and tedius, with the nulls required when there are no children. So, I came up with the approach below to work with your original json file instead, which starts by converting the data to a dictionary, and then using recursion.

#let data = json("file.json").at("information")

#let array_to_dict(data) = {
  let dict = (:)
  let final_indx = data.len() - 1
  for (indx, item) in data.enumerate() {
    if type(item) == array {continue}
    if indx == final_indx or type(data.at(indx + 1)) != array {
      dict.insert(item, none)
    } else {
      dict.insert(item, array_to_dict(data.at(indx + 1)))
    }
  }
  return dict
}

#let list_recursive(dict_data) = {
  for (key, val) in dict_data.pairs() {
    if val == none {
      list.item(key)
    } else {
      list.item[#key #list_recursive(val)]
    }
  }
}

#let data_to_list(array_data) = {
  let dict_data = array_to_dict(array_data)
  list_recursive(dict_data)
}


= Original array data
#data
= Original array data to dict data
#array_to_dict(data)
= Original array data to list
#data_to_list(data)

A simple recursive solution is still possible. As you can see in the structure shown in @miles-1’s reply, sublists are attached to the previous list item. Therefore, you just need to append the sublist to the previous item’s body instead of creating a new one. You also don’t need to explicitly construct a list, as it is created implicitly from the list items.

#let to-list(arr) = {
  arr.fold((), (items, curr) => {
    // If the current entry is an array, append a new sublist to
    // the last item. Otherwise, create a new list item.
    if type(curr) == array { items.at(-1) += to-list(curr) }
    else { items.push(curr) }
    items
  }).map(list.item).join()
}

#let data = json("file.json")
#to-list(data.information)

image

2 Likes