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

Asciidoctor: Support for "open in widget" #627

Merged
merged 14 commits into from
Mar 7, 2019
Merged

Conversation

nik9000
Copy link
Member

@nik9000 nik9000 commented Feb 21, 2019

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.

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
Copy link
Member Author

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.

Copy link
Contributor

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

Copy link
Member Author

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"AsciiDoct" way.
"AsciiDoc" way.

@nik9000
Copy link
Member Author

nik9000 commented Feb 25, 2019

@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

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).

Copy link
Member Author

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.

Copy link
Member Author

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.

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" :)

Copy link
Member Author

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],

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?

Copy link
Member Author

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.

resources/website_common.xsl Outdated Show resolved Hide resolved
resources/website_common.xsl Outdated Show resolved Hide resolved
Copy link
Member Author

@nik9000 nik9000 left a 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],
Copy link
Member Author

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|
Copy link
Member Author

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.

resources/website_common.xsl Outdated Show resolved Hide resolved
resources/website_common.xsl Outdated Show resolved Hide resolved
@nik9000
Copy link
Member Author

nik9000 commented Feb 26, 2019

@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.

@nik9000
Copy link
Member Author

nik9000 commented Mar 6, 2019

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.

I filed #671 to track this.

Copy link

@ddillinger ddillinger left a 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 :)

Copy link
Contributor

@estolfo estolfo left a 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]')
Copy link
Contributor

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'
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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.

@nik9000
Copy link
Member Author

nik9000 commented Mar 7, 2019

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.

@estolfo
Copy link
Contributor

estolfo commented Mar 7, 2019

They're mostly just style suggestions so take or leave what you like. Overall, looks good though!

@nik9000
Copy link
Member Author

nik9000 commented Mar 7, 2019

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....

@nik9000 nik9000 merged commit 772d3c7 into elastic:master Mar 7, 2019
@nik9000
Copy link
Member Author

nik9000 commented Mar 7, 2019

Thanks for reviewing @ddillinger and @estolfo!

@ddillinger
Copy link

@estolfo I sure am glad to see a proper ruby speaker in here, I've kind of just been doing the best I can :)

@estolfo
Copy link
Contributor

estolfo commented Mar 7, 2019

I welcome any chance to nerd out about Ruby and I'm happy to help in any way that I can!

@jarpy
Copy link
Contributor

jarpy commented Mar 8, 2019

A few of them I absorbed from Rubocop

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:

Avoid methods longer than 10 LOC (lines of code).

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.

@nik9000
Copy link
Member Author

nik9000 commented Mar 8, 2019

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.

❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants