zefyr icon indicating copy to clipboard operation
zefyr copied to clipboard

Nested blocks / Indentation

Open pulyaevskiy opened this issue 6 years ago • 6 comments

Description

Allows indenting blocks and individual lines.

Applying to individual lines

When applied to an individual line (without block style) simply indents the line by adding Padding around it with value configurable through ZefyrTheme.

Applying to lines within blocks

When applied to a line with block style following is expected:

1. If all lines in this block share the same style (e.g. quote) visually creates a nested block of the same style for that line. Similarly to how markdown handles nested blocks:

First level quote

Second level quote

etc.

2. If some lines within a block have different style then indented line creates nested block of it's own style. Again similarly to how markdown renders here:

First level quote

  • nested list item

Document model changes

Currently it is required for all lines within a block to have the same block style. This change introduces a need for block nodes to accommodate any group of block-styled lines. Nesting and rendering would be solved outside of the document model in widget layer.

pulyaevskiy avatar Jul 13 '18 04:07 pulyaevskiy

This will most likely introduce a breaking change in the document model, though we should be able to handle it gracefully.

Current thinking is - to change existing block attribute from holding a String value to a more complex object with multiple properties. E.g.:

class BlockProperties {
  final String style; // one of "ul", "ol", "quote", "code" and also new "indent"
  final int indentLevel; // indicates how deeply nested this line in its block
}

Note 1: new "indent" style for blocks. Visually it simply adds indentation to a line or group of lines, without any additional styling. We just treat indented lines is being part of a block too.

Note 2: above change is required to implement "Task Lists". E.g.

  • [ ] Task 1
  • [x] Task 2

This will also require additional field in BlockProperties to hold checked value for the current line.

Backward compatibility:

We'll have to update BlockAttributeBuilder to accept dynamic value, check if it's a String and convert it to the new format on the fly.

pulyaevskiy avatar Sep 16 '19 04:09 pulyaevskiy

Could this be implemented without creating BlockProperties, and by adding line attribute indent instead? BlockNode.optimize and probably others would have to change a bit.

Some API options, taking "decrease indentation" as the one that looks a bit odd in examples:

  • document.format(index, length, NotusAttribute.indent.fromIncrement(-1)); -- probably too surprising/weird.
  • document.increaseFormatBy<T extends num>(index, length, NotusAttribute.indent.fromIncrement(-1)); -- less surprising, and can also increase/decrease heading level or any other int/double attribute.
  • document.indent(index, length, -1); -- simple API, and probably needs to be a specialized function anyway to handle all cases properly.

Probably the last one is best. We'd also have ZefyrController.indentText(...). Thoughts?

jtacoma avatar Nov 22 '20 05:11 jtacoma

question of clarification: Does BlockProperties allow meeting the requirements? If so, how?

"Applying to lines within blocks" shows these examples:

First level quote

Second level quote

First level quote

  • nested list item

Is it a requirement to support both second level quotes and nested list items within a quote? In deltas describing these cases, how would we represent them using BlockProperties?

I'm confused because it looks to me like they would get the same attributes ({"block": {"style": "quote", "indentLevel": 2}}) and we wouldn't be able to tell the difference.

a complex alternative: Use List<String> instead of BlockProperties.

  • {"block": ["quote", "quote"]} = second level quote

  • {"block": ["quote", "ul"]} = nest list item within a quote

  • pro: Meets all requirements.

  • pro: Improves compatibility with Markdown. Markdown supports nesting code blocks within quote blocks within list items, and this alternative would let Notus/Zefyr do that too.

  • con: Does not clear a path for implementing task lists.

a simple alternative: Just add support for NotusAttribute.indent.fromLevel(1).

  • pro: Lines inside or outside of a block can be indented.
  • pro: An indented list item can be displayed as though it is a nested list item.
  • con: No support for nesting a quote or a list item within a quote.
  • con: Does not clear a path for implementing task lists.

an alternative path to support task lists: Allow the registration of custom attributes and block styles.

  • pro: Makes Notus/Zefyr far more powerful: any app that can map its own operational transform type (which may include task states) to and from a Delta with custom attributes can also use Zefyr to provide a familiar UI that feels like a word processor.
  • con: Makes it possible to implement task lists, but it's not obvious how best to do so.

Task lists may be out of scope here, but I want to mention this alternative to suggest that a longer-term plan to support task lists might not need to limit our options here.

jtacoma avatar Nov 23 '20 02:11 jtacoma

@jtacoma Thanks for your ideas, and sorry for the delay.

I agree with your points and those alternatives is something I considered as well. I feel like in the end whenever I try to model just the "indent" feature I always end up trying to solve the "nested blocks" problem.

So I think it'd probably make more sense to focus on the broader solution which would include nested blocks which implies support for indentation as well.

Things I'd like this feature to support:

  • Nesting quote blocks (quotes within quotes) even though it's kind of a weird use case
  • Nesting lists (obviously)
  • Nesting lists inside quote blocks
  • Nesting without any style (which is the same as indenting)
  • Nesting code within quote blocks
  • Nice to have: arbitrary block styles to allow things like callouts (e.g. warnings, notes) with custom styles (icons, headers)

Things I think we shouldn't allow:

  • Nesting code blocks within code blocks (this just makes no sense so should be prevented)
  • Nesting other block styles within lists. E.g. quotes within lists.
  • There is probably more here...

All of the above will require a bunch of changes to the block attribute and corresponding logic, which is why I grouped those all together.

I don't have a solid proposal yet. I'd appreciate any ideas and suggestions though.

pulyaevskiy avatar Jul 29 '21 04:07 pulyaevskiy

As an update my current thinking is to not include this feature in 1.0 as it has a large scope and we don't have a good plan to move forward yet. So maybe it's better to focus on for 2.0 (or 1.x if we can figure out how to make it a non-breaking change).

pulyaevskiy avatar Sep 10 '21 04:09 pulyaevskiy

One approach to resolving this is to split the entire thing into 3 fairly independent sub tasks:

  1. update notus document model to support arbitrary nesting without any logic around which blocks are allowed to be nested in which, max nesting level or anything like that. Basically just the mechanics of reflecting nesting metadata in the Delta and mirroring it in the document tree.
  2. extend existing formatting heuristics system to apply block-level rules. E.g. which kinds of nesting are allowed and which should be prevented, as well as, resolving final formatting result after some editing operations.

The above two would be the bigger and harder chunk of work here and only affect the notus library. The remaining piece:

  1. update zefyr widget layer to be able to render nested blocks. This should be a fairly straightforward part of all 3.

In any case the first thing to figure out would be how block style attribute need to change in order to represent arbitrary nesting.

My current thinking is to record a stack of block styles instead of just a string:

{
  "block": ["quote", "code"]
}

The above example when applied to a line of text in Delta will result in a following equivalent in Markdown:

void main() { print("hello world"); }

That is a code block nested inside a quote block.

Here is a more complex example:

[
  {"insert": "Here is a nested code block"},
  {"insert": "\n", "attributes": {"block": ["quote"]}},
  {"insert": "void main() {}"},
  {"insert": "\n", "attributes": {"block": ["quote", "code"]}},
  {"insert": "And here is a nested list"},
  {"insert": "\n", "attributes": {"block": ["quote"]}},
  {"insert": "First item"},
  {"insert": "\n", "attributes": {"block": ["quote", "ol"]}},
  {"insert": "Second item"},
  {"insert": "\n", "attributes": {"block": ["quote", "ol"]}},
  {"insert": "End of quote"},
  {"insert": "\n", "attributes": {"block": ["quote"]}}
]

The above would translate to:

Here is a nested code block

void main() {}

And here is a nested list

  1. First item
  2. Second item

End of quote

pulyaevskiy avatar Sep 10 '21 04:09 pulyaevskiy