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

feat(gnoweb): Add gno-columns extension #3763

Open
wants to merge 37 commits into
base: master
Choose a base branch
from

Conversation

gfanton
Copy link
Member

@gfanton gfanton commented Feb 16, 2025

Addresses #3255

This PR implements column extension into gnoweb following this format:

-- input.md --
<gno-columns>
## Title 1
content 1
## Title 2
content 2
</gno-columns>

Notes

  • Check testdatas files for example of input/output
  • i've also added an example in /r/demo/mardown_test that need to be improved with usage, example and description (cc @leohhhn)

Preview

example output code

Screenshot 2025-02-17 at 09 36 44

example with images (empty heading)

image

TODO

  • In order to support headings without creating columns, we probably want to use the first heading as a reference for column separator
  • More edge cases for tests
  • Add css to correctly render column in gnoweb (cc @alexiscolin )

@gfanton gfanton requested review from moul and alexiscolin February 16, 2025 21:12
@gfanton gfanton self-assigned this Feb 16, 2025
@github-actions github-actions bot added 🧾 package/realm Tag used for new Realms or Packages. 📦 ⛰️ gno.land Issues or PRs gno.land package related labels Feb 16, 2025
@Gno2D2
Copy link
Collaborator

Gno2D2 commented Feb 16, 2025

🛠 PR Checks Summary

All Automated Checks passed. ✅

Manual Checks (for Reviewers):
  • IGNORE the bot requirements for this PR (force green CI check)
Read More

🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers.

✅ Automated Checks (for Contributors):

🟢 Maintainers must be able to edit this pull request (more info)
🟢 Changes related to gnoweb must be reviewed by its codeowners

☑️ Contributor Actions:
  1. Fix any issues flagged by automated checks.
  2. Follow the Contributor Checklist to ensure your PR is ready for review.
    • Add new tests, or document why they are unnecessary.
    • Provide clear examples/screenshots, if necessary.
    • Update documentation, if required.
    • Ensure no breaking changes, or include BREAKING CHANGE notes.
    • Link related issues/PRs, where applicable.
☑️ Reviewer Actions:
  1. Complete manual checks for the PR, including the guidelines and additional checks if applicable.
📚 Resources:
Debug
Automated Checks
Maintainers must be able to edit this pull request (more info)

If

🟢 Condition met
└── 🟢 And
    ├── 🟢 The base branch matches this pattern: ^master$
    └── 🟢 The pull request was created from a fork (head branch repo: gfanton/gno)

Then

🟢 Requirement satisfied
└── 🟢 Maintainer can modify this pull request

Changes related to gnoweb must be reviewed by its codeowners

If

🟢 Condition met
└── 🟢 And
    ├── 🟢 The base branch matches this pattern: ^master$
    └── 🟢 A changed file matches this pattern: ^gno.land/pkg/gnoweb/ (filename: gno.land/pkg/gnoweb/components/layouts/article.html)

Then

🟢 Requirement satisfied
└── 🟢 Or
    ├── 🟢 This user reviewed pull request: alexiscolin
    └── 🟢 This user reviewed pull request: gfanton

Manual Checks
**IGNORE** the bot requirements for this PR (force green CI check)

If

🟢 Condition met
└── 🟢 On every pull request

Can be checked by

  • Any user with comment edit permission

Signed-off-by: gfanton <[email protected]>
Copy link

codecov bot commented Feb 16, 2025

Codecov Report

Attention: Patch coverage is 79.83539% with 49 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
gno.land/pkg/gnoweb/markdown/ext_column.go 85.11% 25 Missing ⚠️
gno.land/pkg/gnoweb/markdown/golden.go 66.66% 21 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

@alexiscolin
Copy link
Member

Let's go ❤️

@aeddi
Copy link
Contributor

aeddi commented Feb 17, 2025

Cool, looks good :) so you finally decided to split the columns based on the heading or an image right instead of the separator right? (I mean <gno-col-1>, <gno-col-2>)

Honestly, it might be a bit less verbose without the separators, but I find it less intuitive to write/understand and harder to read. When I look at your example comparing the base code and the generated code, I find the generated code way more readable.
Screenshot 2025-02-17 at 09 36 44

It also reduces the flexibility of what content can be added to the columns (something that not start with an image or a heading, or something not consistent between columns).

@alexiscolin
Copy link
Member

@aeddi Yeah I agree regarding the readability but after some discussion in the dedicated issue, it seems people preferred the writing simplicity coupled with an opinionated gnoweb rendering based on headings (not images). To get the images, the hack is to create an empty heading. Please note that <gno-columns> has a shorthand :::.

@aeddi
Copy link
Contributor

aeddi commented Feb 17, 2025

@alexiscolin Ok got it 👍

@gfanton
Copy link
Member Author

gfanton commented Feb 20, 2025

There is one last edge case that I’m unsure how to handle:

<gno-columns>

content 1  
content 2  

## Title 1  

content 3  

## Title 2  

content 4  

</gno-columns>

In this scenario, there is intermediary content between the opening tag and the first heading.
While I would neither recommend nor document this, I want to ensure that any invalid format is handled in a predictable way.

Therefore, I suggest that we always create a first column, regardless of whether a heading is present.

@alexiscolin
Copy link
Member

alexiscolin commented Feb 20, 2025

After some time to think about this, and considering that this edge case occurs because the first encountered title following the opening tag <gno-columns> is used as a separator marker, I agree that we should create a fallback column in this case. But we must ensure all the content before the first heading is embedded into a div.

@alexiscolin
Copy link
Member

alexiscolin commented Feb 28, 2025

I have to check if the case "4 or more cols" can be rendered properly in one line with the current CSS.

@gfanton
Copy link
Member Author

gfanton commented Mar 3, 2025

I've updated with option A in both scenarios.

For Column not starting with a heading : Honestly, I'm not sure about having option B, but it's not like option A is perfect either. This is mostly due to the chosen format, which is ambiguous by nature by reusing the current syntax as a separator, so any solution for intermediary content seems hacky anyway.
Also, don't forget that it is basically an error to not have a heading following the open tag. We should neither document nor support this syntax; we just want a predictable behavior. This also means we can update invalid behavior if it no longer meets our needs.

In addition, I've updated our approach to handling golden tests by using the txtar format. This provides us with a single source of truth for testing. I have also implemented pseudo error handling through an HTML comment.

@aeddi @alexiscolin double-check txtar to see if the output format suits you

Copy link
Contributor

@aeddi aeddi left a comment

Choose a reason for hiding this comment

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

The implementation seems good to me and I think the code generation is more coherent now. Can you just fix the few comments I left you and the lint errors, and then I think we'll be good. 👍

return false
}

fmt.Println(node.Kind())
Copy link
Contributor

Choose a reason for hiding this comment

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

Leftover?

Comment on lines 119 to 121
if len(line) == 0 {
return ColumnTagUndefined
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The check seems unnecessary given the rest of the function.

Copy link
Member Author

Choose a reason for hiding this comment

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

This check has to be done at some point since we have to potentially check the first character of the line later in the function.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh yes, ok, just got the line 135.
WDYT about putting them together then so it's more clear? Like this:

if len(line) > 0 && line[0] == '#' {
		return ColumnTagSep
}

(Or even using strings.HasPrefix)

Comment on lines 87 to 90
IsClose, IsOpen bool
Error error
Index int
RefHeadingLevel int // serves as a level reference for separators
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are these fields exported? Leftover or Is it for encoding (or any similar operation)? Can you unexport them or add a comment about this?

Comment on lines 152 to 155
if cctx.Error != nil {
// Don't bother with malformed block
return nil, parser.NoChildren
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This check seems useless since getColumnContext return a new context if cctx.Error != nil.
For more clarity you could maybe just remove this getColumnContext function and move its content here?

return ColumnTagUndefined
}

// Open opens a new column node based on the separator kind.
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be me misunderstanding, but this comment seems unclear or even incorrect given the content of the function.

Comment on lines 263 to 266
prev, ok := cnode.PreviousSibling().(*ColumnNode)
if !ok || prev.Tag != ColumnTagOpen {
fmt.Fprintln(w, "</div>")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you comment this part? I'm not sure to understand its purpose.

Copy link
Member Author

Choose a reason for hiding this comment

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

it's to avoid having to close a non-existing sep, but re reading this I think it should easier to just check index.


// columnContext struct and its methods are used for handling column context.
type columnContext struct {
IsClose, IsOpen bool
Copy link
Contributor

Choose a reason for hiding this comment

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

After reading the code related to these booleans, the fact that there are two doesn't seem necessary and is confusing.
Why not have a single boolean ⁠IsOpen? (or ⁠IsClosed if you want to init a new columnContext with its false value by default)?

moul added a commit that referenced this pull request Mar 11, 2025
…e content (#3909)

once merged, we should take the habit of updating it when we extend
gnoweb's rendering capabilities such as in #3763

---------

Signed-off-by: moul <[email protected]>
@moul moul changed the title feat(gnoweb): Add column extension feat(gnoweb): Add gno-columns extension Mar 11, 2025
@moul
Copy link
Member

moul commented Mar 11, 2025

i moved the markdown_test realm into r/docs/markdown #3909

can you update this PR?

content 2
</gno-columns>

You can also use ` + "`:::`" + ` as a shortcut for creating columns:
Copy link
Member

Choose a reason for hiding this comment

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

let's start with just <gno-columns>.

::: seems too 'precious' to be an alternative to something that's already concise and clear enough.
if ever we really want it, we can add it in another PR.

"github.com/yuin/goldmark"
)

type gno struct{}
Copy link
Member

Choose a reason for hiding this comment

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

this code is unclear; can you add a comment?


switch cnode.Tag {
case ColumnTagOpen:
fmt.Fprint(w, `<div class="gno-cols">`+"\n")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
fmt.Fprint(w, `<div class="gno-cols">`+"\n")
fmt.Fprint(w, `<div class="gno-columns">`+"\n")

to stay consistent.

Copy link
Member

Choose a reason for hiding this comment

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

ext_columns.go

consistency

Copy link
Member

Choose a reason for hiding this comment

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

ext_columns

consistency

Copy link
Member

Choose a reason for hiding this comment

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

can we avoid having those ".txtar" files that are really different from the other txtar files in the repo?

it looks more like a filetest.gno file in the spirit (one entrypoint, one output).

Copy link
Member

Choose a reason for hiding this comment

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

maybe ".mdtest"

RefHeadingLevel int // serves as a level reference for separators
}

func isPreviousNodeTag(node ast.Node, tag ColumnTag) bool {
Copy link
Member

Choose a reason for hiding this comment

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

deadcode (and in general there are a lot of hard to read code) can you make a quick cleanup?

return ok && cnode.Tag == tag
}

// parseLineTag checks if the line starts with any of the tag.
Copy link
Member

Choose a reason for hiding this comment

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

incorrect, the code checks if the line is a tag, not if it starts with.

it actually doesn't support having a tag and something else on the right, which will be a need for upcoming extensions.


func (*columnParser) Close(_ ast.Node, reader text.Reader, _ parser.Context) {}

func (*columnParser) CanInterruptParagraph() bool {
Copy link
Member

@moul moul Mar 11, 2025

Choose a reason for hiding this comment

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

deadcode? if not add a comment

Copy link
Member Author

Choose a reason for hiding this comment

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

Those methods are for block parsing interface; I will add an assert reference.

return true
}

func (*columnParser) CanAcceptIndentedLine() bool {
Copy link
Member

Choose a reason for hiding this comment

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

deadcode?

const testdataDir = "golden"

var (
update = flag.Bool("update-golden-files", false, "update golden files")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
update = flag.Bool("update-golden-files", false, "update golden files")
update = flag.Bool("update-golden-tests", false, "update golden files")

for consistency

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🌍 gnoweb Issues & PRs related to gnoweb and render 📦 ⛰️ gno.land Issues or PRs gno.land package related 🧾 package/realm Tag used for new Realms or Packages.
Projects
Status: No status
Status: In Review
Development

Successfully merging this pull request may close these issues.

6 participants