-
Notifications
You must be signed in to change notification settings - Fork 10
Refactor NbBlock #235
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
base: main
Are you sure you want to change the base?
Refactor NbBlock #235
Conversation
milestone reached, now in sandbox/minib3.nim both a minimal html and json backend work. (your json work was great @HugoGranstrom). Still lots of details to work on, but I am feeling more confident now! A big realization was that in the end we might not need after all nim mustache anymore (we might still provide it for legacy reason and we should be able to provide a somewhat backward compatible NbLegacyDoc object that implements the default theme as before with partials), in a sense nimib itself is some code based (and block based) templating system with superpowers... |
(moved this note in sandbox/notes.md) |
Mmh, no to customize an existing block the idea is to replace the function that renders it in the nimib/sandbox/minib3/minib.nim Line 26 in 5e6727e
The part above is still an idea and will need more fleshing out. I would definitely start with bare html and would allow customization only in the sense of: give me the rendered output and I can put things before or after. Any kind of refinement to that idea would need a use case that we think makes it worth it. And I would not probably push this during this refactor, maybe for now we do not even do what is sketched above (just to keep the scope manageable).
Yes, that is an option (serializing also the theme), but I guess it is kind of wasteful and also not "clean". I would like if possible to have a somewhat readable json. Especially if it is a low cost thing and a simple rule such as: everything that is in
Yeah, that is a good observation and it might not be needed after all. In principle you should not remember to set it since it should be in the NbBlock generating sugar, but if we can avoid needing it I could probably remove it. It will get trickier once I introduce real custom container blocks. My idea there at the moment is that Thanks for the feedback on the work-in-progress, it is useful :) nothing here is yet set in stone but I think a cleaner implementation is finally coming out. |
note, I just pushed a possible implementation of a NbContainer object (and NbContainer = ref object of NbBlock
blocks: seq[NbBlock]
parent: NbContainer while the html backend works fine with it the json backend fails to serialize because of the circular reference. This is an issue with My current plan is to actually skip the |
ok the new Next steps:
|
Nice work! 🤩 I really like the |
notes from another discussion. in this PR we should also:
|
So, I've started working on implementing Another thing that I'm wondering about is whether we should do the compilation the same way we are doing it now. Now we do it in |
Regarding the partial/backend thing, this is definitely something I am noting myself down to try and help thinking out a solution. This is definitely a breaking change, but I think it should be worth it. Not sure if it helps but the general idea in the past was: a backend is a way to compile to a different format (html vs md vs ...). Backend could support theming and overriding (but basically it made sense for html theme only. Right now we kind of realized that maybe backend abstraction is less customizable than we thought (json backend needs to be baked in). And we could try and keep only a layer of customization at the theme level. Not necessarily it needs to work in the same way (i.e. we could have a way of overriding by inheriting...). Still if, after thinking of what should be the best way to support theming, we figure out also a way to support the old way (mustache had the advantage that you could also change the theme with files that override partials) it would be nice. These are my immediate thoughts and I might be missing a lot. Probably at some point it could be nice to have a lice conversation about this, but maybe in the meantime we could start an async one. |
Thanks for your thoughts 😄 Yes it is a bit of a complicated matter. I still think the backend+ theme is a good way to work. But we need to be a bit more precise in exactly what each part should be responsible for. They way we did it before the refactor was:
One approach going forward I could see is moving the customizable partials to the theme as well. And the backend's responsibility is to render the block from those partials and hardcoded HTML. Having written this though, I don't really see the point in separating them. A theme will generally only work with one backend, so why split them up? Let the theme decide the backend. Or am I missing something here? |
Thinking about it a bit more, I see two kinds of customizations (using nimiSlides as reference)
The first kind of customization is currently possible by modifying toHtml:
withNewlines:
if blk.code.len > 0:
&"<pre><code class=\"nohighlight hljs nim\">{blk.code.highlightNim}</code></pre>"
nbContainerToHtml(blk, nb)
if blk.output.len > 0:
&"<pre class=\"nb-output\">{blk.output}</pre>" Even if I rewrote this to use functions for rendering the code and output, it wouldn't be possible to change it because the function is hardcoded. So I would have to rewrite the function altogether and replace the function calls with my own. So if instead I could do: toHtml:
withNewlines:
nb.renderPartial(blk, "nbCodeSource")
nbContainerToHtml(blk, nb)
nb.renderPartial(blk, "nbCodeOutput") Then I would be able to change the partials and it would change everywhere it is used (in all NbCode-like blocks). BUT the problem is that these two particular partials would need One thing I've realized when writing this is that we aren't really using the inheritance a lot at all when rendering. We are using it for storage and creation. But it feel like we have untapped potential when it comes to rendering. I guess it is hard when you have different backends and want customization. Then we are basically stuck with the table-of-functions that we are using now. So maybe it isn't even desirable to use inherticance when rendering 🤔 This was a lot of rambling and I mostly wrote it down to not forget it hehe😅So no need to reply unless you found something useful. |
all docs are up! (except index.md because of lack of md backend) 🎊 |
I've been thinking a bit more, and I think it boils down to that we want a system that is:
Our options thus far:
I may have missed some options, but currently I see embracing JsonNodes as our best bet. So a typical func somePartial*(blk: JsonNode, nb: Nb): string =
if not blk{"code"}.isNil:
let classes = blk{"classes"}.getElems.mapIt(it.getStr).join(" ")
&"<code class=\"{classes}\">" & blk{"code"}.getStr & "</code>"
else:
""
nb.partials["somePartial"] = somePartial
func someBlockToHtml*(blk: NbBlock, nb: Nb): string =
let blk = blk.SomeBlock
var jObj = blk.toJson
jObj["classes"] = %["highlightjs-nim", "reveal-js-code"]
withNewlines:
"<h1>" & blk.title & "<h1>"
nb.renderPartial("somePartial", jObj) It is more verbose, but I think we could make sugar to simplify the process quite a bit. What do you think? |
The biggest problem with this, I would say is that it is hard to know what fields a particular partial function expects. I don't see how we could handle that at compile-time. But some sugar to check for required and optional fields at runtime should be possible. Then the writer of partials would have to write fewer checks manually at least. |
This is very interesting development, will have to think and reconnect with the actual code to give an informed opinion (current goal is to find time this weekend). Just so I understand, right now we have been implementing inheritance part (and part of the complexity of serializing to json came from that), but according to what you are saying, it is not worth the effort and we should instead revert to something more similar to what we had (only going all in on Json Node)? |
First thought anyway would be to use a |
I think we should keep inheritance (different field for different blocks are good and we get some kind of CT-safety). But for the rendering we don't have much use for it. So there we covert it to JsonNode. But going all in on distinct JsonNode is also an option I haven't considered :O |
Ah ok I was understanding wrong. :) Yeah, all in to JsonNode for rendering looks very reasonable for me. |
Nice to hear. I'll give a try implementing it today and post an example here so we can discuss if we like the syntax and what parts we would like sugarify 😄 |
It was surprisingly easy to implement! And the best thing about this partials stuff is that it's totally optional. If you (a random nimib block creator) don't want to use partials, you can just go your merry way and don't even have to deal with Here's how we implement func nbCodeSourcePartial*(blk: JsonNode, nb: Nb): string =
let code = blk{"code"}.getStr
if code.len > 0:
&"<pre><code class=\"nohighlight hljs nim\">{code.highlightNim}</code></pre>"
else:
""
nbToHtml.partials["nbCodeSource"] = nbCodeSourcePartial
func nbCodeOutputPartial*(blk: JsonNode, nb: Nb): string =
let output = blk{"output"}.getStr
if output.len > 0:
&"<pre class=\"nb-output\">{output}</pre>"
else:
""
nbToHtml.partials["nbCodeOutput"] = nbCodeOutputPartial
newNbBlock(NbCode of NbContainer):
code: string
output: string
toHtml:
withNewlines:
nb.renderPartial("nbCodeSource", jsonutils.toJson(blk))
nbContainerToHtml(blk, nb)
nb.renderPartial("nbCodeOutput", jsonutils.toJson(blk)) It is (as you said above), basically our old mustache-workflow, but with Here's a proposal for a sugar: func nbCodeSource*(nb: Nb, text: string = defaultValue, code: string): string {.nimibPartial.} =
text & code
# rewritten to:
func nbCodeSource*(nb: Nb, blk: JsonNode): string =
let text = blk{"text"}.getStr(defaultValue)
let code = blk{"text"}.getStr(default(string))
text & code It should be pretty straight forward to implement. It helps document the inputs as well as they are part of source code. The handling of the abscence of a certain fields could probably be improved. For example, if no default value is given, maybe we raise an exception as it's a mandatory field? Then the The problematic part is |
Looks good to me! Couple of things I am noticing:
|
Nice! Then I'll go ahead and implement it for the other blocks as well 👍
Exactly,
That is correct. I also didn't know about it until a few weeks ago. It's really nice as it handles var j: JsonNode = nil
j{"hello"}{"world"}.getStr("default string") This will just work, Edit: it can be written even shorter: j{"hello", "world"}.getStr("default string") |
this is a major change, implements #168 (and also #117). Very WIP in a sandbox folder (see sandbox/notes.md)