-
Notifications
You must be signed in to change notification settings - Fork 338
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
Asciidoctor: Support for "open in widget" #627
Conversation
This'll take a while to describe properly. Let's start with the goal first, talk about the AsciiDoc implementation, then move on to the Asciidoctor implementation, then talk about why we need a compatibility layer, then describe the compatibity layer, and finally round out this book of a commit message by describing some of the more esoteric usages of this that this change currently supports and our plan for dropping this support in the future. == The goal Certain snippets in Elastic's docs are special and we'd like to decorate them with buttons. These buttons allow opening the snippets in developer tools or transforming them into `cURL` commands. I'm calling all of the stuff that makes these buttons an "open in widget". == AsciiDoc implementation Because of AsciiDoc's fairly limited ability to customize it we triggered these snippets by adding special comments after the code blocks that we'd like to decorate that look like this: ``` [source,js] ---- GET / ---- // CONSOLE <----- This is the comment ``` We customize AsciiDoc to emit all comments as `<remark>` elements in the generated xml and then use custom xslt to recognize remarks that look like `<remark>CONSOLE</remark>` to trigger the widget. These buttons need the contents of the snippet, `GET /` in the example above, to be accessible in a file. We implement this by post-processing the generated html in the perl code to extra these snippets to files, rewriting the html that generates the widget to point to the file. == Asciidoctor implementation Asciidoctor feels strongly that comments shouldn't have semantic meaning. I'm on board with that. So, to trigger the widget in Asciidoctor you use: ``` [source,console] ---- GET / ---- ``` Note that the language, which was `js` in AsciiDoc is now `console`. This language is what triggers the widget. This feels good to me because all snippets that are in the "console" language really do want this decoration. And because the snippets really *aren't* javascript. They just often have json in them. We recognize these `:listing` blocks using a treepreprocessor and built the snippet file when processing the Asciidoctor AST. We also rewrite the docbook that is generated by these blocks to contain a link to the extract snippet. Finally, we also use custom xslt to extract that link and render the widget. The xslt is less hairy than the one used to find the `<remark>`s. == Why we need compatibility It'd be fairly simple to change `// CONSOLE` to `[source,console]` on a page by page basis, but we have thousands of uses of `// CONSOLE` across dozens of books and dozens of branches. In addition, AsciiDoc doesn't understand `[source,console]` which'd make changing these a thing that you'd have to do at the same time as you switched the book from AsciiDoc to Asciidoctor. Beyond *that*, Elasticsearch automatically extracts snippets with the `// CONSOLE` comment and turns them into tests. There are just too many moving parts for a hard cut over from `// CONSOLE` to `[source,console]`. So I built a compatiblity layer in Asciidoctor == How the compatibilty layer works We use two customizations in Asciidoctor to make the compatibility layer go, one that recognizes comments shaped like `// CONSOLE` and turns them into a literal `// CONSOLE` using the `pass` macro like so: `pass:[// CONSOLE]`. The next one picks up source blocks that are immediately followed by `pass:[// CONSOLE]` and switches the language to `console`. Something like this *begs* for away to tell the user "you have 1232 warnings that you have to fix". Like linting but for your docs. I'll be thinking more about this in the coming weeks. But for now there is not warning when you use the compatibility layer. == Esoteric forms of the "open in widget" Kibana's docs have snippets like: ``` [source,js] -------------------------------------------------- GET api/logstash/pipeline/hello-world -------------------------------------------------- // KIBANA ``` Note the `// KIBANA`. This is *like* `// CONSOLE` but it is for snippets that should be sent to Kibana's API instead of Elasticsearch's. It is debateable if `kibana` is really a different language than `console`, but it is at least a different dialect. Either way, Asciidoctor triggers the "open in widget" for these snippets with `[source,kibana]`. Older versions of the documentation have snippets like this: ``` [source,js] -------------------------------------------------- GET / -------------------------------------------------- // AUTOSENSE ``` These open the command in an older tool named "sense" instead of the developer console. The developer console "grew out of" sense but hasn't been called that for a long time. In any case, it exists for compatibility with old books. The Definitive Guide has snippets that look like: ``` [source,js] -------------------------------------------------- GET / -------------------------------------------------- // SENSE:a/path/to/some.json ``` These are snippets that render as `GET /` but when you open them in "sense" they have the contents of `a/path/to/some.json` which is supposed to be similar. This isn't widely used and I personally think it is super confusing, at least the way that we've implemented it now. So I log a warning whenever you try to do this which should prevent folks from doing it accidentally. == Follow ups This work begs for at least two follow up changes: 1. A warnings management system so old books can continue to use the backwards compatibility features but new books will be forced to use native Asciidoctor features. 2. More in depth integration testing. Right now we build README.asciidoc with AsciiDoc and Asciidoctor which is great, but it is difficult to assert that the results are "close enough" with the tools that we have. We can do better and this change makes it obvious that we must do better.
@@ -1051,38 +1083,6 @@ The local web browser can be stopped with `Ctrl-C`. | |||
|
|||
================================ | |||
|
|||
==== Custom Console snippets |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've dropped the docs for this because I think, at least as we have it now, it is super confusing. It isn't compatible with Elasticsearch's tests or COPY AS cURL
and I expect it is pretty surprising in general.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason these custom blocks exist is for the Definitive Guide, where we wanted to display a small amount of JSON but then click through to console to show the full example, including setup etc. Other than the Def Guide, it is not really used anywhere else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah! I tracked that down.
We've talked on and off about having some way of hiding something like these custom blocks but in a way with visual feedback for what you are getting. I'm not sure the right way to do it but I could image these making a comeback eventually.
But as I have it now I'm just dropping the docs for them because I don't want people using them without really knowing what they are doing.
README.asciidoc
Outdated
the Elaticsearch repository because it can recognize it to make tests. The | ||
"Asciidoctor" way is preferred in other books, but only if they are built with | ||
"Asciidoctor". Try it first and if it works then use it. Otherwise, use the | ||
"AsciiDoct" way. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"AsciiDoct" way. | |
"AsciiDoc" way. |
@ddillinger, could you add this to your review queue? I think the idea is sound even if it does create two ways to do it. I think this is one of those things like the inline callouts where we will have to change. But unlike the inline callouts we'd change after switching to asciidoctor because the compatibility layer isn't too funky and because we have zillions of these. |
if File.readable? normalized | ||
copy_snippet block, normalized, snippet_path | ||
else | ||
logger.warn message_with_context "can't read snippet from #{normalized}", :source_location => block.source_location |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is warn
sufficient here, or should it fail more aggressively? This looks like a pretty bad state to be in and seems like it would produce broken docs (pieces missing).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
warn
is the level that Asciidoctor fails when it can't include a referenced file. We have to fail the asciidoctor run if it emits any warnings. Which, now that you mention them, would be another great integration test. Which I'll do in a separate PR because it'll have a bigger splash radius.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm - I've rechecked this. Asciidoctor actually spits out an ERROR
for missing files. I can switch to that. But it still keeps going which isn't really a terrible thing because it allows you to collect all of the failures. Sometimes that results in kind of a mess, but often it is quite helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah for sure that is a better behavior, we just also really want error
when something goes as sideways as "an entire whole thing was just not there" :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
asciidoc
is the one that warns! asciidoctor makes an error..... I had my programs crossed.
[ | ||
%w[CONSOLE console], | ||
%w[AUTOSENSE sense], | ||
%w[KIBANA kibana], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unlike the other test, this one lacks a SENSE
case; is that okay?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmmmm...... When I wrote this I distinctly remember thinking that it was ok. On further reflection it is not OK. We use SENSE here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for reviewing @ddillinger! I'll fix it up and then get to work on the integration test for warnings.
[ | ||
%w[CONSOLE console], | ||
%w[AUTOSENSE sense], | ||
%w[KIBANA kibana], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmmmm...... When I wrote this I distinctly remember thinking that it was ok. On further reflection it is not OK. We use SENSE here.
} | ||
end | ||
|
||
%w[console sense kibana].each do |lang| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I is ok that this doesn't have autosense
because this widget only support sense
. autosense
is transformed into sense
in tree compatibility processor.
And shift the warning for being hard to read to after the copy.
@ddillinger I switched the WARNING to an ERROR. It still keeps going, C compiler style, which is normal for asciidoctor and asciidoc. And, generally, is pretty useful even if it can make a mess sometimes. |
I filed #671 to track this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the issue :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Nik, I made some comments regarding minor style things and use of return
. I mentioned in a comment on the rspec tests that I'm happy to discuss further in a call as a comment wouldn't be sufficient. Overall, this looks good to me. Isn't Ruby great?? ; )
# CONSOLE snippet. Asciidoctor really doesn't recommend this sort of | ||
# thing but we have thousands of them and it'll take us some time to | ||
# stop doing it. | ||
line&.gsub!(%r{//\s*(?:AUTOSENSE|KIBANA|CONSOLE|SENSE:[^\n<]+)}, 'pass:[\0]') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would consider making the regexp a constant so that it's not instantiated each time this code is called. It also has the benefit of following this file's convention of making regexp's constants. See these lines
block.subs << :specialcharacters | ||
block.subs << :callouts if had_callouts | ||
end | ||
return unless block.context == :listing && block.style == 'source' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a minor Ruby style thing, but I would typically only use return
if there were a specific value I was returning when a condition was met. For example, here
In this case, I would write the code like this:
if block.context == :listing && block.style == 'source'
process_subs block
process_lang_override block
end
end | ||
|
||
def process_subs(block) | ||
return if block.subs.include? :specialcharacters |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing here. I would write this like:
if !block.subs.include? :specialcharacters
# callouts have to come *after* special characters
had_callouts = block.subs.delete(:callouts)
block.subs << :specialcharacters
block.subs << :callouts if had_callouts
end
block.set_attr 'snippet', snippet | ||
|
||
block.parent.blocks.delete next_block | ||
block.parent.reindex_sections |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be more readable if all the logic was in one place. The same things I said above about return
hold true here. I would write this code like:
def process_lang_override(block)
next_block = block.next_adjacent_block
unless next_block && next_block.context == :paragraph &&
next_block.source =~ %r{pass:\[//\s*([^:\]]+)(?::\s*([^\]]+))?\]} &&
lang = LANG_MAPPING[$1]
snippet = $2
block.set_attr 'language', lang
block.set_attr 'snippet', snippet
block.parent.blocks.delete next_block
block.parent.reindex_sections
end
end
return unless block.context == :listing && block.style == 'source' | ||
|
||
lang = block.attr 'language' | ||
return unless %w[console sense kibana].include? lang |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would put the following in a unless %w[console sense kibana].include? lang
condition.
end | ||
else | ||
# If you don't specify the snippet then we assign it a number and read | ||
# if from the contents of the source listing, copying it to the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a minor typo: it from the contents
block.document.register :links, snippet_path | ||
|
||
def block.content | ||
@attributes['snippet_link'] + super |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could also write this like "#{@attributes['snippet_link']}#{super}"
It doesn't really matter, I just use string interpolation whenever I can : )
WARNINGS | ||
actual = convert input, stub_file_opts, eq(warnings.strip) | ||
expect(actual).to eq(expected.strip) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that your additions to this rspec file are consistent with the style of the rest of the tests but you can use let
blocks and shared_examples
to make it more readable and easier to extend in the future. I'm happy to discuss further in a call or in chat, as I don't think I can explain it very well in this comment.
Thanks @estolfo! I'm going to go through and correct a bunch of these. A few of them I absorbed from Rubocop and talking to you a few weeks ago and have grown to like. So I'm not going to change them for now and I'll let them soak into my brain for a bit first. |
They're mostly just style suggestions so take or leave what you like. Overall, looks good though! |
Thanks! Mostly I've grown attached to the "early return" thing.... |
Thanks for reviewing @ddillinger and @estolfo! |
@estolfo I sure am glad to see a proper ruby speaker in here, I've kind of just been doing the best I can :) |
I welcome any chance to nerd out about Ruby and I'm happy to help in any way that I can! |
There are some really interesting ideas embodied in Rubocop, and by inference The Ruby Style Guide. This one, in particular, I think has fascinating results:
When I first saw that pop up in my editor, I was actually angry. "I can't believe this", I thought. "They're enforcing pretentious code-golf behaviour in the lint checker!". But, I thought I should be empirical and try doing a whole project in accordance with everything in the style guide and Rubocop... The effect of this one particular rule was revelatory. I realised it wasn't about code-golf at all, but about so comprehensively decomposing everything that you end up "accidentally" creating a lovely little grammar for your application that consists of all these delightfully simple little functions. Trying out this rule from The Ruby Style Guide, even if I no longer apply it strictly, made me a better programmer in every language. |
❤️ |
This'll take a while to describe properly. Let's start with the goal
first, talk about the AsciiDoc implementation, then move on to the
Asciidoctor implementation, then talk about why we need a compatibility
layer, then describe the compatibity layer, and finally round out this
book of a commit message by describing some of the more esoteric usages
of this that this change currently supports and our plan for dropping
this support in the future.
== The goal
Certain snippets in Elastic's docs are special and we'd like to decorate
them with buttons. These buttons allow opening the snippets in developer
tools or transforming them into
cURL
commands. I'm calling all of thestuff that makes these buttons an "open in widget".
== AsciiDoc implementation
Because of AsciiDoc's fairly limited ability to customize it we
triggered these snippets by adding special comments after the code
blocks that we'd like to decorate that look like this:
We customize AsciiDoc to emit all comments as
<remark>
elements in thegenerated xml and then use custom xslt to recognize remarks that look
like
<remark>CONSOLE</remark>
to trigger the widget.These buttons need the contents of the snippet,
GET /
in the exampleabove, to be accessible in a file. We implement this by post-processing
the generated html in the perl code to extra these snippets to files,
rewriting the html that generates the widget to point to the file.
== Asciidoctor implementation
Asciidoctor feels strongly that comments shouldn't have semantic
meaning. I'm on board with that. So, to trigger the widget in
Asciidoctor you use:
Note that the language, which was
js
in AsciiDoc is nowconsole
.This language is what triggers the widget. This feels good to me because
all snippets that are in the "console" language really do want this
decoration. And because the snippets really aren't javascript. They
just often have json in them.
We recognize these
:listing
blocks using a treepreprocessor and builtthe snippet file when processing the Asciidoctor AST. We also rewrite
the docbook that is generated by these blocks to contain a link to the
extract snippet. Finally, we also use custom xslt to extract that link
and render the widget. The xslt is less hairy than the one used to find
the
<remark>
s.== Why we need compatibility
It'd be fairly simple to change
// CONSOLE
to[source,console]
on apage by page basis, but we have thousands of uses of
// CONSOLE
acrossdozens of books and dozens of branches. In addition, AsciiDoc doesn't
understand
[source,console]
which'd make changing these a thing thatyou'd have to do at the same time as you switched the book from AsciiDoc
to Asciidoctor. Beyond that, Elasticsearch automatically extracts
snippets with the
// CONSOLE
comment and turns them into tests.There are just too many moving parts for a hard cut over from
// CONSOLE
to[source,console]
. So I built a compatiblity layer inAsciidoctor
== How the compatibilty layer works
We use two customizations in Asciidoctor to make the compatibility layer
go, one that recognizes comments shaped like
// CONSOLE
and turns theminto a literal
// CONSOLE
using thepass
macro like so:pass:[// CONSOLE]
. The next one picks up source blocks that areimmediately followed by
pass:[// CONSOLE]
and switches the language toconsole
.Something like this begs for away to tell the user "you have 1232
warnings that you have to fix". Like linting but for your docs. I'll be
thinking more about this in the coming weeks. But for now there is not
warning when you use the compatibility layer.
== Esoteric forms of the "open in widget"
Kibana's docs have snippets like:
Note the
// KIBANA
. This is like// CONSOLE
but it is for snippetsthat should be sent to Kibana's API instead of Elasticsearch's. It is
debateable if
kibana
is really a different language thanconsole
,but it is at least a different dialect. Either way, Asciidoctor triggers
the "open in widget" for these snippets with
[source,kibana]
.Older versions of the documentation have snippets like this:
These open the command in an older tool named "sense" instead of the
developer console. The developer console "grew out of" sense but hasn't
been called that for a long time. In any case, it exists for
compatibility with old books.
The Definitive Guide has snippets that look like:
These are snippets that render as
GET /
but when you open them in"sense" they have the contents of
a/path/to/some.json
which issupposed to be similar. This isn't widely used and I personally think it
is super confusing, at least the way that we've implemented it now. So I
log a warning whenever you try to do this which should prevent folks
from doing it accidentally.
== Follow ups
This work begs for at least two follow up changes:
backwards compatibility features but new books will be forced to use
native Asciidoctor features.
README.asciidoc with AsciiDoc and Asciidoctor which is great, but it is
difficult to assert that the results are "close enough" with the tools
that we have. We can do better and this change makes it obvious that we
must do better.