Skip to content

Commit 772d3c7

Browse files
authored
Asciidoctor: Support for "open in widget" (#627)
* Asciidoctor: Support for "open in widget" 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. * Word * Clean up after merge * Missing command * speeling * Shift to error And shift the warning for being hard to read to after the copy. * Test cleanups * Style
1 parent bb25b75 commit 772d3c7

File tree

16 files changed

+539
-61
lines changed

16 files changed

+539
-61
lines changed

README.asciidoc

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,12 +1014,26 @@ footnote to a particular line of code:
10141014
=== View in Console
10151015

10161016
Code blocks can be followed by a "View in Console" link which, when clicked,
1017-
will open the code snippet in Console. The snippet can either be taken directly
1018-
from the code block (`CONSOLE`), or be a link to a custom snippet.
1017+
will open the code snippet in Console. There are two ways to do this, the
1018+
"AsciiDoc" way and the "Asciidoctor" way. The "AsciiDoc" way is preferred in
1019+
the Elaticsearch repository because it can recognize it to make tests. The
1020+
"Asciidoctor" way is preferred in other books, but only if they are built with
1021+
"Asciidoctor". Try it first and if it works then use it. Otherwise, use the
1022+
"AsciiDoc" way.
10191023

1020-
.Code block with CONSOLE link
1024+
////
1025+
1026+
The tricks that we pull to make ascidoctor support // CONSOLE are windy
1027+
and force us to add subs=+macros when we render an asciidoc snippet.
1028+
We *don't* require that for normal snippets, just those that contain
1029+
asciidoc.
1030+
1031+
////
1032+
1033+
.Code block with CONSOLE link (AsciiDoc way)
10211034
==================================
1022-
[source,asciidoc]
1035+
ifdef::asciidoctor[[source,asciidoc,subs=+macros]]
1036+
ifndef::asciidoctor[[source,asciidoc]]
10231037
--
10241038
[source,js]
10251039
----------------------------------
@@ -1035,6 +1049,24 @@ GET /_search
10351049
==================================
10361050
<1> The `// CONSOLE` line must follow immediately after the code block, before any callouts.
10371051

1052+
.Code block with CONSOLE link (Asciidoctor way)
1053+
==================================
1054+
[source,asciidoc]
1055+
--
1056+
[source,console]
1057+
----------------------------------
1058+
GET /_search
1059+
{
1060+
"query": "foo bar" \<1>
1061+
}
1062+
----------------------------------
1063+
1064+
\<1> Here's the explanation
1065+
--
1066+
==================================
1067+
1068+
Both render as:
1069+
10381070
[source,js]
10391071
----------------------------------
10401072
GET /_search
@@ -1060,38 +1092,6 @@ The local web browser can be stopped with `Ctrl-C`.
10601092
10611093
================================
10621094

1063-
==== Custom Console snippets
1064-
1065-
Sometimes you will want to show a small amount of code in the code block, but
1066-
to provide a full recreation in the Console snippet. In this case, you need to:
1067-
1068-
* Save the snippet file in the `./snippets/` directory in the root docs directory.
1069-
* Under the code block, specify the name of the snippet file with
1070-
+
1071-
// CONSOLE: path/to/snippet.json
1072-
1073-
For instance, to add a custom snippet to the file `./one/two/three.asciidoc`, save the snippet
1074-
to `./snippets/one/two/three/example_1.json`, then add the `CONSOLE` link below the code block:
1075-
1076-
.Code block with custom CONSOLE link
1077-
==================================
1078-
[source,asciidoc]
1079-
--
1080-
[source,js]
1081-
----------------------------------
1082-
GET /_search
1083-
{
1084-
"query": "foo bar" \<1>
1085-
}
1086-
----------------------------------
1087-
// CONSOLE:one/two/three/example_1.json <1>
1088-
1089-
\<1> Here's the explanation
1090-
--
1091-
<1> The path should not contain the initial `snippets` directory
1092-
==================================
1093-
1094-
10951095
[[admon-blocks]]
10961096
=== Admonition blocks
10971097

integtest/Makefile

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,15 @@ experimental_expected_files: /tmp/experimental_asciidoc
6363
%_same_files: /tmp/%_asciidoc /tmp/%_asciidoctor
6464
diff \
6565
<(cd /tmp/$*_asciidoc && find * -type f | sort \
66-
| grep -v snippets/blocks \
66+
| grep -v 'snippets/' \
6767
) \
68-
<(cd /tmp/$*_asciidoctor && find * -type f | sort)
69-
# The grep -v below are for known issues with asciidoctor
70-
for file in $$(cd /tmp/$*_asciidoc && find * -type f -name '*.html' \
71-
| grep -v 'blocks'); do \
68+
<(cd /tmp/$*_asciidoctor && find * -type f | sort \
69+
| grep -v 'snippets/' \
70+
)
71+
for file in $$(cd /tmp/$*_asciidoc && find * -type f -name '*.html'); do \
7272
./html_diff /tmp/$*_asciidoc/$$file /tmp/$*_asciidoctor/$$file; \
7373
done
74+
# TODO validate the snippets have the same contents even if the files aren't the same
7475

7576
# Build the docs into the target
7677
define BD=

integtest/html_diff

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@ def normalize_html(html):
3535
# is between words. They are actively nice to have but asciidoc doesn't
3636
# make them.
3737
html = html.replace('\u2014\u200b', '\u2014')
38-
# Temporary workaround for known issues
39-
html = re.sub(
40-
r'(?m)^\s+<div class="console_widget" data-snippet="[^"]+">'
41-
r'\s+</div>\n', '', html)
38+
# We intentionally changed lang-js to lang-console because in Asciidoctor
39+
# because that is more accurate
40+
html = html.replace('"programlisting prettyprint lang-js"',
41+
'"programlisting prettyprint lang-console"')
42+
# The URL for the console snippets has changed
43+
html = re.sub(r'data-snippet="[^"]+"', 'data-snippet="snippet"', html)
44+
# Temporary work around for known issue
4245
html = html.replace('\\&lt;1&gt;', '&lt;1&gt;')
4346
return html
4447

lib/ES/Template.pm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ sub apply {
4747
my $self = shift;
4848
my $dir = shift;
4949
my $lang = shift || die "No lang specified";
50+
my $asciidoctor = shift;
5051

5152
my $map = $self->_map;
5253

@@ -61,7 +62,7 @@ sub apply {
6162
$contents =~ s/\s*$/\n/;
6263

6364
# Extract AUTOSENSE snippets
64-
$contents = $self->_autosense_snippets( $file, $contents );
65+
$contents = $self->_autosense_snippets( $file, $contents ) unless $asciidoctor;
6566

6667
# Fill in template
6768
my @parts = @{ $self->_parts };

lib/ES/Util.pm

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ sub build_chunked {
154154
my ($chunk_dir) = grep { -d and /\.chunked$/ } $dest->children
155155
or die "Couldn't find chunk dir in <$dest>";
156156

157-
finish_build( $index->parent, $chunk_dir, $lang );
157+
finish_build( $index->parent, $chunk_dir, $lang, $asciidoctor );
158158
extract_toc_from_index($chunk_dir);
159159
for ( $chunk_dir->children ) {
160160
run( 'mv', $_, $dest );
@@ -287,7 +287,7 @@ sub build_single {
287287
or die "Couldn't rename <$src> to <index.html>: $!";
288288
}
289289

290-
finish_build( $index->parent, $dest, $lang );
290+
finish_build( $index->parent, $dest, $lang, $asciidoctor );
291291
}
292292

293293
#===================================
@@ -369,10 +369,10 @@ sub build_pdf {
369369
#===================================
370370
sub finish_build {
371371
#===================================
372-
my ( $source, $dest, $lang ) = @_;
372+
my ( $source, $dest, $lang, $asciidoctor ) = @_;
373373

374374
# Apply template to HTML files
375-
$Opts->{template}->apply( $dest, $lang );
375+
$Opts->{template}->apply( $dest, $lang, $asciidoctor );
376376

377377
my $snippets_dest = $dest->subdir('snippets');
378378
my $snippets_src;

resources/asciidoctor/lib/elastic_compat_preprocessor/extension.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,31 @@
8989
# Because Asciidoc permits these mismatches but asciidoctor does not. We'll
9090
# emit a warning because, permitted or not, they are bad style.
9191
#
92+
# With the help of ElasticCompatTreeProcessor turns
93+
# [source,js]
94+
# ----
95+
# foo
96+
# ----
97+
# // CONSOLE
98+
#
99+
# Into
100+
# [source,console]
101+
# ----
102+
# foo
103+
# ----
104+
# Because Elastic has thousands of these constructs but Asciidoctor feels
105+
# strongly that comments should not convey meaning. This is a totally
106+
# reasonable stance and we should migrate away from these comments in new
107+
# docs when it is possible. But for now we have to support the comments as
108+
# well.
109+
#
92110
class ElasticCompatPreprocessor < Asciidoctor::Extensions::Preprocessor
93111
include Asciidoctor::Logging
94112

95113
INCLUDE_TAGGED_DIRECTIVE_RX = /^include-tagged::([^\[][^\[]*)\[(#{Asciidoctor::CC_ANY}+)?\]$/.freeze
96114
SOURCE_WITH_SUBS_RX = /^\["source", ?"[^"]+", ?subs="(#{Asciidoctor::CC_ANY}+)"\]$/.freeze
97115
CODE_BLOCK_RX = /^-----*$/.freeze
116+
SNIPPET_RX = %r{//\s*(?:AUTOSENSE|KIBANA|CONSOLE|SENSE:[^\n<]+)}.freeze
98117

99118
def process(_document, reader)
100119
reader.instance_variable_set :@in_attribute_only_block, false
@@ -142,13 +161,22 @@ def reader.process_line(line)
142161
@code_block_start = line
143162
end
144163
end
164+
145165
supported = 'added|beta|coming|deprecated|experimental'
146166
# First convert the "block" version of these macros. We convert them
147167
# to block macros because they are at the start of the line....
148168
line&.gsub!(/^(#{supported})\[([^\]]*)\]/, '\1::[\2]')
149169
# Then convert the "inline" version of these macros. We convert them
150170
# to inline macros because they are *not* at the start of the line....
151171
line&.gsub!(/(#{supported})\[([^\]]*)\]/, '\1:[\2]')
172+
173+
# Transform Elastic's traditional comment based marking for
174+
# AUTOSENSE/KIBANA/CONSOLE snippets into a marker that we can pick
175+
# up during tree processing to turn the snippet into a marked up
176+
# CONSOLE snippet. Asciidoctor really doesn't recommend this sort of
177+
# thing but we have thousands of them and it'll take us some time to
178+
# stop doing it.
179+
line&.gsub!(SNIPPET_RX, 'pass:[\0]')
152180
end
153181
end
154182
reader

resources/asciidoctor/lib/elastic_compat_tree_processor/extension.rb

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,62 @@
2323
# <1> The count of categories that were matched
2424
# <2> The categories retrieved
2525
#
26+
# Turns
27+
# [source,js]
28+
# --------------------------------------------------
29+
# GET / <1>
30+
# --------------------------------------------------
31+
# pass:[// CONSOLE]
32+
# <1> The count of categories that were matched
33+
# <2> The categories retrieved
34+
#
35+
# Into
36+
# [source,console]
37+
# --------------------------------------------------
38+
# GET / <1>
39+
# --------------------------------------------------
40+
# <1> The count of categories that were matched
41+
# <2> The categories retrieved
42+
#
2643
class ElasticCompatTreeProcessor < TreeProcessorScaffold
44+
include Asciidoctor::Logging
45+
2746
def process_block(block)
28-
if block.context == :listing && block.style == "source" &&
29-
block.subs.include?(:specialcharacters) == false
30-
# callouts have to come *after* special characters
31-
had_callouts = block.subs.delete(:callouts)
32-
block.subs << :specialcharacters
33-
block.subs << :callouts if had_callouts
34-
end
47+
return unless block.context == :listing && block.style == 'source'
48+
49+
process_subs block
50+
process_lang_override block
51+
end
52+
53+
def process_subs(block)
54+
return if block.subs.include? :specialcharacters
55+
56+
# callouts have to come *after* special characters
57+
had_callouts = block.subs.delete(:callouts)
58+
block.subs << :specialcharacters
59+
block.subs << :callouts if had_callouts
60+
end
61+
62+
LANG_MAPPING = {
63+
'AUTOSENSE' => 'sense',
64+
'CONSOLE' => 'console',
65+
'KIBANA' => 'kibana',
66+
'SENSE' => 'sense',
67+
}.freeze
68+
69+
def process_lang_override(block)
70+
next_block = block.next_adjacent_block
71+
return unless next_block && next_block.context == :paragraph
72+
return unless next_block.source =~ %r{pass:\[//\s*([^:\]]+)(?::\s*([^\]]+))?\]}
73+
74+
lang = LANG_MAPPING[$1]
75+
snippet = $2
76+
return unless lang # Not a language we handle
77+
78+
block.set_attr 'language', lang
79+
block.set_attr 'snippet', snippet
80+
81+
block.parent.blocks.delete next_block
82+
block.parent.reindex_sections
3583
end
3684
end

resources/asciidoctor/lib/extensions.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require_relative 'elastic_compat_tree_processor/extension'
99
require_relative 'elastic_compat_preprocessor/extension'
1010
require_relative 'elastic_include_tagged/extension'
11+
require_relative 'open_in_widget/extension'
1112

1213
Asciidoctor::Extensions.register CareAdmonition
1314
Asciidoctor::Extensions.register ChangeAdmonition
@@ -20,5 +21,6 @@
2021
treeprocessor CopyImages::CopyImages
2122
treeprocessor EditMe
2223
treeprocessor ElasticCompatTreeProcessor
24+
treeprocessor OpenInWidget
2325
include_processor ElasticIncludeTagged
2426
end

0 commit comments

Comments
 (0)