asciidoctor-sail 0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3c02b2d48c65bd2e04e77fe09135f4ac55b41567e3ed6cfa8ca477ac650fe064
4
+ data.tar.gz: 5519b40e6be23cf4388d4ce15940f142ba3ee956f3027512ebd3d349d9a96816
5
+ SHA512:
6
+ metadata.gz: 217a7edc3c5b47ef72261cc21330949ec8b368863ae18857cf18f038c72e6ddcea6e6f27ff50c9c79809a8680d898840e99509063994fca58a069c500c741819
7
+ data.tar.gz: 8a9f7db2589280023e8e219b2db0ede1f546aa530c80b8bf3c8bddbe18e383ae7a6416b2e73d9f421540d81bb3fe59ebccd39ec02325d4e3535b5c34a093366b
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2023 Alasdair Armstrong
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.adoc ADDED
@@ -0,0 +1,7 @@
1
+ = Sail Asciidoctor Plugin
2
+
3
+ An https://asciidoctor.org[Asciidoctor] extension for including code from
4
+ https://github.com/rems-project/sail[Sail] ISA specifications into
5
+ Asciidoc documents, primarily aimed at supporting documentation for
6
+ RISC-V extensions using the
7
+ https://github.com/riscv/sail-riscv[sail-riscv] model.
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rouge'
4
+
5
+ # We define a rouge lexer for Sail source
6
+ class SailLexer < Rouge::RegexLexer
7
+ title 'Sail'
8
+ desc 'Sail ISA Description Language (https://github.com/rems-project/sail)'
9
+ tag 'sail'
10
+ filenames '*.sail'
11
+
12
+ id = /[a-zA-Z_?][a-zA-Z0-9_?#]*/
13
+
14
+ tyvar = /'[a-zA-Z_?][a-zA-Z0-9_?#]*/
15
+
16
+ # We are careful with the definition of operators to ensure openers
17
+ # like // and /* cannot prefix valid operators
18
+ op_char = '[!%&*+-./:<=>@^|]'
19
+ op_char_no_slash = '[!%&*+-.:<=>@^|]'
20
+ op_char_no_slash_star = '[!%&+-.:<=>@^|]'
21
+
22
+ # Sail operators can be suffixed with an underscore followed by a
23
+ # regular identifier i.e. <=_u for unsigned less that or equal to
24
+ op_suffix = "#{op_char}*(_#{id.source})"
25
+
26
+ # Operators of length 1, 2, and n > 2
27
+ operator1 = Regexp.new(op_char)
28
+ operator2 = Regexp.new("(#{op_char}#{op_char_no_slash_star})|(#{op_char_no_slash}#{op_char})")
29
+ operatorn = Regexp.new("(#{op_char}#{op_char_no_slash_star}#{op_suffix})|(#{op_char_no_slash}#{op_char}#{op_suffix})")
30
+
31
+ def self.keywords
32
+ @keywords ||= Set.new %w[
33
+ and as by match clause operator default end enum else forall foreach function mapping overload throw
34
+ try catch if in let var ref pure monadic register return scattered struct then type union newtype with
35
+ val outcome instantiation impl repeat until while do bitfield forwards backwards to
36
+ ]
37
+ end
38
+
39
+ # Keywords that appear in types, and builtin special types
40
+ def self.keywords_type
41
+ @keywords_type ||= Set.new %w[
42
+ dec inc Int Order Bool Type bits bool int option unit implicit
43
+ ]
44
+ end
45
+
46
+ # These keywords appear like special functions rather than regular
47
+ # keywords, i.e. `assert(cond, "message")`
48
+ def self.builtins
49
+ @builtins ||= Set.new %w[
50
+ bitzero bitone exit false sizeof constraint true undefined
51
+ ]
52
+ end
53
+
54
+ # Reserved and internal keywords, as well as deprecated keywords
55
+ def self.reserved
56
+ @reserved ||= Set.new %w[
57
+ effect cast constant import module mutual configuration termination_measure internal_plet internal_return
58
+ ]
59
+ end
60
+
61
+ state :whitespace do
62
+ rule(/\s+/, Text::Whitespace)
63
+ end
64
+
65
+ state :root do
66
+ mixin :whitespace
67
+
68
+ rule(/0x[0-9A-Fa-f_]+/, Num::Hex)
69
+ rule(/0b[0-1_]+/, Num::Bin)
70
+ rule(/[0-9]+\.[0-9]+/, Num::Float)
71
+ rule(/[0-9_]+/, Num::Integer)
72
+
73
+ rule(/"/, Str, :string)
74
+
75
+ rule tyvar, Name::Variable
76
+
77
+ rule(/(val\b)(\s+)(#{id})/) do
78
+ groups Keyword, Text::Whitespace, Name::Function
79
+ end
80
+
81
+ rule(/(function\b)(\s+)(#{id})/) do
82
+ groups Keyword, Text::Whitespace, Name::Function
83
+ end
84
+
85
+ rule %r{//}, Comment, :line_comment
86
+ rule %r{/\*}, Comment, :comment
87
+ rule(/\$\[/, Comment::Preproc, :attribute)
88
+ rule(/\$#{id}/, Comment::Preproc, :pragma)
89
+
90
+ # Function arrows
91
+ rule(/->/, Punctuation)
92
+
93
+ # Two character brackets
94
+ rule(/\[\|/, Punctuation)
95
+ rule(/\|\]/, Punctuation)
96
+ rule(/{\|/, Punctuation)
97
+ rule(/\|}/, Punctuation)
98
+
99
+ rule(/[,@=(){}\[\];:]/, Punctuation)
100
+ rule operatorn, Operator
101
+ rule operator2, Operator
102
+ rule operator1, Operator
103
+
104
+ rule id do |m|
105
+ name = m[0]
106
+
107
+ if self.class.keywords.include? name
108
+ token Keyword
109
+ elsif self.class.keywords_type.include? name
110
+ token Keyword::Type
111
+ elsif self.class.builtins.include? name
112
+ token Name::Builtin
113
+ elsif self.class.reserved.include? name
114
+ token Keyword::Reserved
115
+ else
116
+ token Name
117
+ end
118
+ end
119
+ end
120
+
121
+ state :string do
122
+ rule(/"/, Str, :pop!)
123
+ # Sail escape sequences are a subset of OCaml's https://v2.ocaml.org/manual/lex.html#escape-sequence
124
+ rule(/\\([\\ntbr"']|x[a-fA-F0-9]{2}|[0-7]{3})/, Str::Escape)
125
+ rule(/[^\\"\n]+/, Str)
126
+ end
127
+
128
+ state :attribute do
129
+ rule(/\]/, Comment::Preproc, :pop!)
130
+ rule(/[^\]]+/, Comment::Preproc)
131
+ end
132
+
133
+ state :pragma do
134
+ rule(/\n/, Text::Whitespace, :pop!)
135
+ rule(/[^\n]+/, Comment::Preproc)
136
+ end
137
+
138
+ state :line_comment do
139
+ rule(/\n/, Text::Whitespace, :pop!)
140
+ rule(/[^\n]+/, Comment)
141
+ end
142
+
143
+ state :comment do
144
+ rule %r{/\*}, Comment, :comment
145
+ rule %r{\*/}, Comment, :pop!
146
+ rule(/\n/, Text::Whitespace)
147
+ rule(/./, Comment)
148
+ end
149
+ end
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Asciidoctor
4
+ module Sail
5
+ module SourceMacro
6
+ # Should match Docinfo.docinfo_version in Sail OCaml source
7
+ VERSION = 1
8
+ PLUGIN_NAME = 'Sail Asciidoc plugin'
9
+
10
+ def get_sourcemap(doc, attrs)
11
+ from = attrs.delete('from') { 'sail-doc' }
12
+ source_map = doc.attr(from)
13
+ ::Asciidoctor::Sail::Sources.register(from, source_map)
14
+ json = ::Asciidoctor::Sail::Sources.get(from)
15
+ if json['version'] != VERSION
16
+ raise "#{PLUGIN_NAME}: Version does not match version in source map #{source_map}"
17
+ end
18
+
19
+ json
20
+ end
21
+
22
+ def get_type(attrs)
23
+ attrs.delete('type') { 'function' }
24
+ end
25
+
26
+ def get_part(attrs)
27
+ attrs.delete('part') { 'source' }
28
+ end
29
+
30
+ def get_split(attrs)
31
+ attrs.delete('split') { '' }
32
+ end
33
+
34
+ def read_source(json, part)
35
+ source = ''
36
+
37
+ if json.is_a? String
38
+ source = json
39
+ elsif json[part].is_a? String
40
+ source = json[part]
41
+ else
42
+ file = File.read(json['file'])
43
+ loc = json[part]['loc']
44
+
45
+ # Get the source code, adjusting for the indentation of the first line of the span
46
+ indent = loc[2] - loc[1]
47
+
48
+ source = file.byteslice(loc[2], loc[5] - loc[2])
49
+ source = (' ' * indent) + source
50
+ end
51
+
52
+ source
53
+ end
54
+
55
+ def get_sail_object(json, target, attrs)
56
+ type = get_type(attrs)
57
+ json = json["#{type}s"]
58
+ if json.nil?
59
+ raise "#{PLUGIN_NAME}: No Sail objects of type #{type}"
60
+ end
61
+ json = json[target]
62
+ if json.nil?
63
+ raise "#{PLUGIN_NAME}: No Sail #{type} #{target} could be found"
64
+ end
65
+ json = json[type]
66
+
67
+ if attrs.key? 'clause'
68
+ clause = attrs.delete('clause')
69
+ json.each do |child|
70
+ if match_clause(clause, child['pattern'])
71
+ json = child
72
+ break
73
+ end
74
+ end
75
+ elsif attrs.key? 'left-clause'
76
+ clause = attrs.delete('left-clause')
77
+ json.each do |child|
78
+ if match_clause(clause, child['left'])
79
+ json = child
80
+ break
81
+ end
82
+ end
83
+ elsif attrs.key? 'right-clause'
84
+ clause = attrs.delete('right-clause')
85
+ json.each do |child|
86
+ if match_clause(clause, child['right'])
87
+ json = child
88
+ break
89
+ end
90
+ end
91
+ elsif attrs.key? 'grep'
92
+ grep = attrs.delete('grep')
93
+ json.each do |child|
94
+ source = read_source(child, 'body')
95
+ if source =~ Regexp.new(grep)
96
+ json = child
97
+ end
98
+ end
99
+ end
100
+
101
+ json
102
+ end
103
+
104
+ # Compute the minimum indentation for any line in a source block
105
+ def minindent(tabwidth, source)
106
+ indent = -1
107
+ source.each_line do |line|
108
+ line_indent = 0
109
+ line.chars.each do |c|
110
+ case c
111
+ when ' '
112
+ line_indent += 1
113
+ when "\t"
114
+ line_indent += tabwidth
115
+ else
116
+ break
117
+ end
118
+ end
119
+ indent = line_indent if indent == -1 || line_indent < indent
120
+ end
121
+ indent
122
+ end
123
+
124
+ def get_source(doc, target, attrs)
125
+ json = get_sourcemap doc, attrs
126
+ json = get_sail_object json, target, attrs
127
+ dedent = attrs.any? { |k, v| (k.is_a? Integer) && %w[dedent unindent].include?(v) }
128
+ strip = attrs.any? { |k, v| (k.is_a? Integer) && %w[trim strip].include?(v) }
129
+
130
+ part = get_part attrs
131
+ split = get_split attrs
132
+
133
+ source = ''
134
+ if split != ''
135
+ source = json['splits'][split]
136
+ else
137
+ source = read_source(json, part)
138
+ end
139
+
140
+ source.strip! if strip
141
+
142
+ if dedent
143
+ lines = ''
144
+ min = minindent 4, source
145
+
146
+ source.each_line do |line|
147
+ lines += line[min..]
148
+ end
149
+ source = lines
150
+ end
151
+
152
+ source
153
+ end
154
+
155
+ def match_clause(desc, json)
156
+ if desc =~ /^([a-zA-Z_?][a-zA-Z0-9_?#]*)(\(.*\))$/
157
+ return false unless json['type'] == 'app' && json['id'] == ::Regexp.last_match(1)
158
+
159
+ patterns = json['patterns']
160
+ if patterns.length == 1
161
+ patterns = patterns[0]
162
+ end
163
+
164
+ match_clause ::Regexp.last_match(2), patterns
165
+ elsif desc.length.positive? && desc[0] == '('
166
+ tuples = nil
167
+ if json.is_a? Array
168
+ tuples = json
169
+ elsif json['type'] == 'tuple'
170
+ tuples = json['patterns']
171
+ else
172
+ tuples = [json]
173
+ end
174
+
175
+ results = []
176
+ desc[1...-1].split(',').each_with_index do |desc, i|
177
+ results.push(match_clause(desc.strip, tuples[i]))
178
+ end
179
+ results.all?
180
+ elsif desc == '_'
181
+ true
182
+ elsif desc =~ /^([a-zA-Z_?][a-zA-Z0-9_?#]*)$/
183
+ json['type'] == 'id' && json['id'] == ::Regexp.last_match(1)
184
+ elsif desc =~ /^(0[bx][a-fA-F0-9]*)$/
185
+ json['type'] == 'literal' && json['value'] == ::Regexp.last_match(1)
186
+ else
187
+ false
188
+ end
189
+ end
190
+ end
191
+
192
+ class SourceBlockMacro < ::Asciidoctor::Extensions::BlockMacroProcessor
193
+ include SourceMacro
194
+
195
+ use_dsl
196
+
197
+ named :sail
198
+
199
+ def process(parent, target, attrs)
200
+ source = get_source parent.document, target, attrs
201
+
202
+ create_listing_block parent, source, { 'style' => 'source', 'language' => 'sail' }
203
+ end
204
+ end
205
+
206
+ class SourceIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor
207
+ include SourceMacro
208
+
209
+ def handles? target
210
+ target.start_with? 'sail:'
211
+ end
212
+
213
+ def process doc, reader, target, attrs
214
+ target.delete_prefix! 'sail:'
215
+
216
+ source = get_source doc, target, attrs
217
+
218
+ reader.push_include source, target, target, 1, {}
219
+ reader
220
+ end
221
+ end
222
+
223
+ class WavedromIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor
224
+ include SourceMacro
225
+
226
+ def handles? target
227
+ target.start_with? 'sailwavedrom:'
228
+ end
229
+
230
+ def process doc, reader, target, attrs
231
+ target.delete_prefix! 'sailwavedrom:'
232
+ json = get_sourcemap doc, attrs
233
+ json = get_sail_object json, target, attrs
234
+
235
+ key = 'wavedrom'
236
+ if attrs.any? { |k, v| (k.is_a? Integer) && v == 'right' }
237
+ key = 'right_wavedrom'
238
+ elsif attrs.any? { |k, v| (k.is_a? Integer) && v == 'left' }
239
+ key = 'left_wavedrom'
240
+ end
241
+
242
+ if attrs.key? 'raw'
243
+ diagram = json[key]
244
+ else
245
+ diagram = "[wavedrom, , svg]\n....\n#{json[key]}\n...."
246
+ end
247
+
248
+ reader.push_include diagram, target, target, 1, {}
249
+ reader
250
+ end
251
+ end
252
+
253
+ class DocCommentIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor
254
+ include SourceMacro
255
+
256
+ def handles? target
257
+ target.start_with? 'sailcomment:'
258
+ end
259
+
260
+ def process doc, reader, target, attrs
261
+ target.delete_prefix! 'sailcomment:'
262
+ json = get_sourcemap doc, attrs
263
+ json = get_sail_object json, target, attrs
264
+
265
+ if json.nil? || json.is_a?(Array)
266
+ raise "#{PLUGIN_NAME}: Could not find Sail object for #{target} when processing include::sailcomment. You may need to specify a clause."
267
+ end
268
+
269
+ comment = json['comment']
270
+ if comment.nil?
271
+ raise "#{PLUGIN_NAME}: No documentation comment for Sail object #{target}"
272
+ end
273
+
274
+ reader.push_include comment, target, target, 1, attrs
275
+ reader
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Asciidoctor
6
+ module Sail
7
+ class Sources
8
+ @sources = {}
9
+
10
+ def self.register(key, sourcemap_path)
11
+ return if @sources.key?(key)
12
+
13
+ file = File.read(sourcemap_path)
14
+ @sources[key] = JSON.parse(file)
15
+ end
16
+
17
+ def self.get(key)
18
+ @sources[key]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'asciidoctor/extensions'
4
+
5
+ require_relative 'asciidoctor-sail/sources'
6
+ require_relative 'asciidoctor-sail/macros'
7
+ require_relative 'asciidoctor-sail/highlighter'
8
+
9
+ Asciidoctor::Extensions.register do
10
+ block_macro Asciidoctor::Sail::SourceBlockMacro
11
+ include_processor Asciidoctor::Sail::SourceIncludeProcessor
12
+ include_processor Asciidoctor::Sail::DocCommentIncludeProcessor
13
+ include_processor Asciidoctor::Sail::WavedromIncludeProcessor
14
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: asciidoctor-sail
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Alasdair Armstrong
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-03-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Asciidoctor extension for documenting Sail models
14
+ email:
15
+ - alasdair.armstrong@cl.cam.ac.uk
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.adoc
22
+ - lib/asciidoctor-sail.rb
23
+ - lib/asciidoctor-sail/highlighter.rb
24
+ - lib/asciidoctor-sail/macros.rb
25
+ - lib/asciidoctor-sail/sources.rb
26
+ homepage: https://github.com/Alasdair/asciidoctor-sail
27
+ licenses:
28
+ - MIT
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.4.9
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: An Asciidoctor extension that supports including formatted Sail source in
49
+ ISA manuals
50
+ test_files: []