elisp2any 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -15,12 +15,22 @@ $ elisp2any --input /path/to/input/file --output /path/to/output/file
15
15
 
16
16
  ## Development
17
17
 
18
- After checking out the repo, run `bin/setup` to install dependencies.
19
- Then, run `rake test` to run the tests.
20
- You can also run `bin/console` for an interactive prompt that will allow you to experiment.
18
+ Two paragraph types:
21
19
 
22
- To install this gem onto your local machine, run `bundle exec rake install`.
23
- To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
20
+ ```emacs-lisp
21
+ ;; This is a major one.
22
+
23
+ ;; This is another major one (1), and a minor one.
24
+ ;;
25
+ ;; This is still (1) but another minor one.
26
+ ```
27
+
28
+ Heading levels:
29
+
30
+ ```emacs-lisp
31
+ ;;; Level 2
32
+ ;;;; Level 3
33
+ ```
24
34
 
25
35
  ## Contributing
26
36
 
data/Rakefile CHANGED
@@ -7,49 +7,4 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList['test/**/*_test.rb']
8
8
  end
9
9
 
10
- default_tasks = %i[test]
11
-
12
- rubocop = true
13
-
14
- begin
15
- require 'rubocop/rake_task'
16
- rescue LoadError
17
- rubocop = false
18
- end
19
-
20
- if rubocop
21
- RuboCop::RakeTask.new
22
- default_tasks << :rubocop
23
- end
24
-
25
- task default: default_tasks
26
-
27
- require 'rdoc/task'
28
- RDoc::Task.new do |rdoc|
29
- readme = 'README.md'
30
- rdoc.main = readme
31
- rdoc.rdoc_files.include(readme, 'lib/**/*.rb')
32
- end
33
-
34
- desc 'start API document server'
35
- task :serve_api do
36
- serve('html')
37
- end
38
-
39
- desc 'generate type signatures'
40
- task :sig do
41
- sh 'typeprof', *Dir['lib/**/*.rb'], 'sig/elisp2any.rbs', '-o', 'sig/elisp2any.gen.rbs'
42
- end
43
-
44
- desc 'start HTML fixture server'
45
- task :serve_fixture do
46
- serve('fixtures/init')
47
- end
48
-
49
- def serve(path)
50
- sh 'ruby', '-run', '-e', 'httpd', path
51
- end
52
-
53
- file 'fixtures/init/adoc/index.html' => 'fixtures/init/index.adoc' do |t|
54
- sh 'asciidoctor', t.source, '--out-file', t.name
55
- end
10
+ task default: :test
data/exe/elisp2any CHANGED
@@ -3,13 +3,11 @@
3
3
  require 'optparse'
4
4
 
5
5
  $LOAD_PATH << File.join(__dir__, "../lib")
6
- require 'elisp2any'
7
- require 'elisp2any/html_renderer'
6
+ require 'elisp'
8
7
 
9
8
  input = $stdin
10
9
  output = $stdout
11
10
  css = nil
12
- mode = :old
13
11
 
14
12
  OptionParser.new do |parser|
15
13
  parser.on('--input=PATH') do |path|
@@ -23,18 +21,6 @@ OptionParser.new do |parser|
23
21
  parser.on('--css=PATH') do |path|
24
22
  css = path
25
23
  end
26
-
27
- parser.on("--new") do
28
- mode = :new
29
- end
30
24
  end.parse!
31
25
 
32
- file = nil
33
- case mode
34
- in :new
35
- file = Elisp2any::File.scan(input)
36
- in :old
37
- file = Elisp2any::File.parse(input)
38
- end
39
- renderer = Elisp2any::HTMLRenderer.new(file, css:, mode:)
40
- output.write(renderer.render)
26
+ Elisp.parse(input).write(output, css:)
@@ -0,0 +1,93 @@
1
+ class Elisp::Comment
2
+ def initialize(content)
3
+ @content = content
4
+ end
5
+
6
+ def <<(content)
7
+ @content << "\n#{content}"
8
+ end
9
+
10
+ def to_minor_para
11
+ Elisp::MinorPara.new(@content)
12
+ end
13
+
14
+ def html
15
+ self.class.parse(@content)
16
+ end
17
+ end
18
+
19
+ class Elisp::DocStringParser
20
+ URI_PATTERN = URI.regexp
21
+ URI_PATTERN_WITH_ANGLES = /[<](?<uri>#{ URI_PATTERN })[>]/
22
+ EMAIL = Regexp.new(URI::MailTo::EMAIL_REGEXP.to_s
23
+ .sub(/\A[(][?]-mix:\\A/, "(?-mix:")
24
+ .sub(/\\z[)]\z/, ")"))
25
+ EMAIL_WITH_ANGLES = /[<](?<address>#{ EMAIL })[>]/
26
+
27
+ def initialize(source)
28
+ @scanner = StringScanner.new(source)
29
+ end
30
+
31
+ def html
32
+ result = +""
33
+ normal = +""
34
+ until @scanner.eos?
35
+ if @scanner.skip(/[`](?<code>[A-Za-z!.-]+)[']/)
36
+ result << normal
37
+ normal.clear
38
+ code = CGI.escape_html(@scanner[:code])
39
+ result << "<code>#{code}</code>"
40
+ normal.clear
41
+ elsif @scanner.skip(/\\\\=(?<quote>['`])/)
42
+ normal << @scanner[:quote]
43
+ elsif (uri = scan_http_uri)
44
+ result << normal
45
+ normal.clear
46
+ result << html_url(uri)
47
+ elsif (address = scan_email)
48
+ result << normal
49
+ normal.clear
50
+ uri = CGI.escape(address)
51
+ address = CGI.escape_html(address)
52
+ result << %(<a href="mailto:#{uri}">#{address}</a>)
53
+ else
54
+ normal << @scanner.getch
55
+ end
56
+ end
57
+ result << normal
58
+ normal.clear
59
+ result
60
+ end
61
+
62
+ def scan_email
63
+ if (address = @scanner.scan(EMAIL))
64
+ address
65
+ elsif @scanner.skip(EMAIL_WITH_ANGLES)
66
+ @scanner[:address]
67
+ else
68
+ return
69
+ end
70
+ end
71
+
72
+ def scan_http_uri
73
+ if (uri = @scanner.scan(URI_PATTERN))
74
+ elsif @scanner.skip(URI_PATTERN_WITH_ANGLES)
75
+ uri = @scanner[:uri]
76
+ else
77
+ return
78
+ end
79
+ uri = URI(uri)
80
+ unless uri.is_a?(URI::HTTP)
81
+ @scanner.unscan
82
+ return
83
+ end
84
+ uri
85
+ end
86
+
87
+ def html_url(url)
88
+ url = url.to_s
89
+ ref = CGI.escape(url)
90
+ label = CGI.escape_html(url)
91
+ %(<a class="pure-url" href="#{ref}">#{label}</a>)
92
+ end
93
+ end
@@ -0,0 +1,188 @@
1
+ class Elisp::Parser
2
+ HEADER_LINE = %r{
3
+ ;;;[ ]
4
+ (?<title>[a-z]+[.]el)[ ]---[ ](?<desc>.*?)
5
+ (?:[ ]+-[*]-[ ](?<vars>lexical-binding:[ ]?t);?[ ]-[*]-)?
6
+ \n+
7
+ }x
8
+ COMMENT = / *;; (?<comment>.*)\n/
9
+ CODE = %r{
10
+ (?<content>
11
+ (?:
12
+ [A-Za-z0-9&,.:=?_#'`<>()\[\]\t +*/-]
13
+ | [?]\\.
14
+ | "(?:[^\\"]|\\(?:.|\n))*"
15
+ )+?
16
+ (?:[ ]*;.*)?
17
+ | ;;;[#][#][#]autoload.*
18
+ )
19
+ \n
20
+ }x
21
+ BLANKLINES = /\n+/
22
+ CODE_OR_BLANKLINES = Regexp.union(CODE, BLANKLINES)
23
+ DEFAULT_CSS = <<~END_CSS
24
+ <style>
25
+ :root {
26
+ --sidebar-width: 200px;
27
+ }
28
+ body {
29
+ display: flex;
30
+ }
31
+ main {
32
+ max-width: 80rem;
33
+ margin: auto auto auto var(--sidebar-width);
34
+ padding-left: 0.5rem;
35
+ border-box: box-sizing;
36
+ }
37
+ .sidebar {
38
+ padding-right: 0.5rem;
39
+ width: var(--sidebar-width);
40
+ position: fixed;
41
+ boxed-sizing: border-box;
42
+ overflow-y: auto;
43
+ height: 100%;
44
+ }
45
+ .sidebar details ul {
46
+ position: relative;
47
+ left: -1.5rem;
48
+ }
49
+ .description {
50
+ font-style: italic;
51
+ }
52
+ pre {
53
+ font-family: serif;
54
+ }
55
+ pre[class="code"] {
56
+ border-left: solid;
57
+ padding-left: 0.5rem;
58
+ white-space: pre-wrap;
59
+ }
60
+ </style>
61
+ END_CSS
62
+ HEADING = %r{
63
+ ;;;(?<level>;*)[ ](?<title>.+)\n
64
+ (?:(?:;;)?\n)*
65
+ }x
66
+
67
+ def initialize(input)
68
+ @scanner = StringScanner.new(input.read)
69
+ @state = :start
70
+ @nodes = []
71
+ end
72
+
73
+ def parse
74
+ until @scanner.eos?
75
+
76
+ if @state == :start && @scanner.skip(HEADER_LINE)
77
+ @nodes << Elisp::Heading.new(@scanner[:title], level: 1)
78
+ @nodes << Elisp::Desc.new(@scanner[:desc])
79
+ @nodes << Elisp::Variables.new(@scanner[:vars])
80
+ @state = :after_header_line
81
+
82
+ elsif [:after_header_line,
83
+ :after_major_para,
84
+ :after_minor_para,
85
+ :after_code,
86
+ :after_page_delimiter,
87
+ :after_heading].include?(@state) &&
88
+ @scanner.skip(HEADING)
89
+ level = @scanner[:level].size
90
+ title = @scanner[:title]
91
+ if level.zero?
92
+ if ["Commentary:", "Code:"].include?(title)
93
+ title.chop!
94
+ elsif title.match?(/[a-z]+[.]el ends here/)
95
+ next # validate file name?
96
+ end
97
+ end
98
+ @nodes << Elisp::Heading.new(title, level: level + 2)
99
+ @state = :after_heading
100
+
101
+ elsif [:after_heading,
102
+ :after_major_para,
103
+ :after_minor_para,
104
+ :after_code,
105
+ :after_header_line].include?(@state) &&
106
+ @scanner.skip(COMMENT)
107
+ @nodes << Elisp::Comment.new(@scanner[:comment])
108
+ @state = :after_comment
109
+
110
+ elsif @state == :after_comment && @scanner.skip(";;\n")
111
+ @nodes[-1] = @nodes.last.to_minor_para
112
+ @state = :after_minor_para
113
+
114
+ elsif @state == :after_comment && @scanner.skip(COMMENT)
115
+ @nodes.last << @scanner[:comment]
116
+
117
+ elsif @state == :after_comment && @scanner.skip(BLANKLINES)
118
+ make_major_para
119
+ @state = :after_major_para
120
+
121
+ elsif [:after_major_para, :after_page_delimiter, :after_heading].include?(@state) &&
122
+ @scanner.skip(CODE)
123
+ @nodes << Elisp::Code.new(@scanner[:content])
124
+ @state = :after_code
125
+
126
+ elsif @state == :after_comment && @scanner.skip(CODE)
127
+ make_major_para
128
+ @nodes << Elisp::Code.new(@scanner[:content])
129
+ @state = :after_code
130
+
131
+ elsif @state == :after_code && @scanner.skip(CODE_OR_BLANKLINES)
132
+ @nodes.last << @scanner[:content]
133
+
134
+ elsif @state == :after_code && @scanner.skip(/\f\n+/)
135
+ @nodes << PageDelimiter.new
136
+ @state = :after_page_delimiter
137
+
138
+ else
139
+ raise Elisp::Error, <<~MESSAGE
140
+ parse failed
141
+ --- rest line (#{@state}) ---
142
+ #{@scanner.rest.lines.first.inspect}
143
+ --- scanner ---
144
+ #{@scanner.inspect}
145
+ MESSAGE
146
+ end
147
+ end
148
+ self
149
+ end
150
+
151
+ def make_major_para
152
+ paras = [@nodes.pop.to_minor_para]
153
+ while (node = @nodes.pop)
154
+ if node.is_a?(Elisp::MinorPara)
155
+ paras.unshift(node)
156
+ else
157
+ @nodes.push(node)
158
+ break
159
+ end
160
+ end
161
+ @nodes << Elisp::MajorPara.new(paras)
162
+ end
163
+
164
+ def write(output, css: nil)
165
+ css = css ? %(<link rel="stylesheet" href="#{CGI.escape(css)}">) : DEFAULT_CSS
166
+ sidebar = Elisp::Sidebar.new(@nodes.select { |node| node.is_a?(Elisp::Heading) \
167
+ && node.level >= 2 })
168
+ .html
169
+ body = @nodes.map(&:html).join("\n")
170
+ output.write(<<~END_HTML)
171
+ <!doctype html>
172
+ <html>
173
+ <head>
174
+ #{css}
175
+ <meta charset="utf8">
176
+ </head>
177
+ <body>
178
+ <nav class="sidebar">
179
+ #{sidebar}
180
+ </nav>
181
+ <main>
182
+ #{body}
183
+ </main>
184
+ </body>
185
+ </html>
186
+ END_HTML
187
+ end
188
+ end
data/lib/elisp.rb ADDED
@@ -0,0 +1,142 @@
1
+ require "strscan"
2
+ require "cgi"
3
+ require "uri"
4
+
5
+ class Elisp
6
+ Error = Class.new(StandardError)
7
+
8
+ def self.parse(input)
9
+ Elisp::Parser.new(input).parse
10
+ end
11
+ end
12
+
13
+ class Elisp::Sidebar
14
+ def initialize(headings)
15
+ @headings = headings
16
+ end
17
+
18
+ def html
19
+ rest = @headings.dup
20
+ result = +""
21
+ while (head = rest.shift)
22
+ level = head.level
23
+ subheadings = []
24
+ while (heading = rest.shift)
25
+ if heading.level > level
26
+ subheadings << heading
27
+ else
28
+ rest.unshift(heading)
29
+ break
30
+ end
31
+ end
32
+ sub_sidebar =
33
+ subheadings.empty? ? nil
34
+ : (html = self.class.new(subheadings).html
35
+ "<details open>#{html}</details>")
36
+ content = CGI.escape_html(head.content)
37
+ result << <<~END_HTML
38
+ <li>
39
+ <a href="##{head.html_id}">#{content}</a>
40
+ #{sub_sidebar}
41
+ </li>
42
+ END_HTML
43
+ end
44
+ "<ul>#{result}</ul>"
45
+ end
46
+ end
47
+
48
+ class PageDelimiter
49
+ def html = "<hr>"
50
+ end
51
+
52
+ class Elisp::Heading
53
+ attr_reader :level, :content
54
+
55
+ def initialize(content, level: 2)
56
+ @content = content
57
+ @level = level
58
+ end
59
+
60
+ def html
61
+ name = "h#{@level}"
62
+ content = CGI.escape_html(@content)
63
+ <<~END_HTML
64
+ <#{name} id="#{html_id}">
65
+ #{content}
66
+ <a href="##{html_id}">#</a>
67
+ </#{name}>
68
+ END_HTML
69
+ end
70
+
71
+ def html_id
72
+ content = CGI.escape(@content)
73
+ "#{@level}-#{content}"
74
+ end
75
+ end
76
+
77
+ class Elisp::Desc
78
+ def initialize(content)
79
+ @content = content
80
+ end
81
+
82
+ def html
83
+ content = CGI.escape_html(@content)
84
+ %(<p class="description">#{content}</p>)
85
+ end
86
+ end
87
+
88
+ class Elisp::Variables
89
+ def initialize(content)
90
+ @content = content
91
+ end
92
+
93
+ def html
94
+ content = CGI.escape_html(@content)
95
+ <<~END_HTML
96
+ <article>
97
+ Header line variables:
98
+ <pre><code>#{content}</code></pre>
99
+ </article>
100
+ END_HTML
101
+ end
102
+ end
103
+
104
+ class Elisp::MajorPara
105
+ def initialize(paras)
106
+ @paras = paras
107
+ end
108
+
109
+ def html
110
+ paras = @paras.map(&:html).join("\n")
111
+ "<article>#{paras}</article>"
112
+ end
113
+ end
114
+
115
+ class Elisp::MinorPara
116
+ def initialize(content)
117
+ @content = content
118
+ end
119
+
120
+ def html
121
+ content = Elisp::DocStringParser.new(@content).html
122
+ "<pre>#{content}</pre>"
123
+ end
124
+ end
125
+
126
+ class Elisp::Code
127
+ def initialize(content)
128
+ @content = content
129
+ end
130
+
131
+ def <<(content)
132
+ @content << "\n#{content}"
133
+ end
134
+
135
+ def html
136
+ content = CGI.escape_html(@content)
137
+ %(<pre class="code"><code>#{content}</code></pre>)
138
+ end
139
+ end
140
+
141
+ require_relative "elisp/comment"
142
+ require_relative "elisp/parser"
@@ -1,4 +1,4 @@
1
1
  module Elisp2any
2
2
  # Version of this library.
3
- VERSION = '0.0.5'
3
+ VERSION = '0.0.6'
4
4
  end
data/lib/elisp2any.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require_relative 'elisp2any/version'
2
- require_relative 'elisp2any/file'
3
2
  require_relative 'elisp2any/heading'
4
3
 
5
4
  module Elisp2any
metadata CHANGED
@@ -1,43 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elisp2any
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - gemmaro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-10 00:00:00.000000000 Z
11
+ date: 2025-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: ruby_tree_sitter
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: 0.20.8.1
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: 0.20.8.1
27
- - !ruby/object:Gem::Dependency
28
- name: asciidoctor
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
13
  - !ruby/object:Gem::Dependency
42
14
  name: rake
43
15
  requirement: !ruby/object:Gem::Requirement
@@ -52,20 +24,6 @@ dependencies:
52
24
  - - "~>"
53
25
  - !ruby/object:Gem::Version
54
26
  version: '13.0'
55
- - !ruby/object:Gem::Dependency
56
- name: rubocop
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '1.21'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '1.21'
69
27
  - !ruby/object:Gem::Dependency
70
28
  name: test-unit
71
29
  requirement: !ruby/object:Gem::Requirement
@@ -80,22 +38,8 @@ dependencies:
80
38
  - - "~>"
81
39
  - !ruby/object:Gem::Version
82
40
  version: '3.0'
83
- - !ruby/object:Gem::Dependency
84
- name: webrick
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
41
  description: elisp2any is a command line tool and library for converting Emacs Lisp
98
- source to some document markup, such as HTML or AsciiDoc.
42
+ source to some document markup, such as HTML.
99
43
  email:
100
44
  - gemmaro.dev@gmail.com
101
45
  executables:
@@ -103,10 +47,6 @@ executables:
103
47
  extensions: []
104
48
  extra_rdoc_files: []
105
49
  files:
106
- - ".document"
107
- - ".envrc"
108
- - ".rdoc_options"
109
- - ".rubocop.yml"
110
50
  - CHANGELOG.md
111
51
  - Gemfile
112
52
  - LICENSE.txt
@@ -114,9 +54,10 @@ files:
114
54
  - Rakefile
115
55
  - exe/elisp2any
116
56
  - fixtures/init.el
57
+ - lib/elisp.rb
58
+ - lib/elisp/comment.rb
59
+ - lib/elisp/parser.rb
117
60
  - lib/elisp2any.rb
118
- - lib/elisp2any/asciidoc_renderer.rb
119
- - lib/elisp2any/asciidoc_renderer/index.adoc.erb
120
61
  - lib/elisp2any/aside.rb
121
62
  - lib/elisp2any/blanklines.rb
122
63
  - lib/elisp2any/code.rb
@@ -124,7 +65,6 @@ files:
124
65
  - lib/elisp2any/comment.rb
125
66
  - lib/elisp2any/commentary.rb
126
67
  - lib/elisp2any/expression.rb
127
- - lib/elisp2any/file.rb
128
68
  - lib/elisp2any/footer_line.rb
129
69
  - lib/elisp2any/header_line.rb
130
70
  - lib/elisp2any/heading.rb
@@ -133,18 +73,13 @@ files:
133
73
  - lib/elisp2any/html_renderer/index.html.erb
134
74
  - lib/elisp2any/inline_code.rb
135
75
  - lib/elisp2any/line.rb
136
- - lib/elisp2any/node.rb
137
76
  - lib/elisp2any/paragraph.rb
138
77
  - lib/elisp2any/section.rb
139
78
  - lib/elisp2any/text.rb
140
- - lib/elisp2any/tree_sitter_parser.rb
141
79
  - lib/elisp2any/version.rb
142
- - manifest.scm
143
- - sig/elisp2any.gen.rbs
144
- - sig/elisp2any.rbs
145
80
  homepage: https://codeberg.org/gemmaro/elisp2any
146
81
  licenses:
147
- - Apache-2.0
82
+ - GPL-3.0-or-later
148
83
  metadata:
149
84
  rubygems_mfa_required: 'true'
150
85
  bug_tracker_uri: https://codeberg.org/gemmaro/elisp2any/issues
@@ -168,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
103
  - !ruby/object:Gem::Version
169
104
  version: '0'
170
105
  requirements: []
171
- rubygems_version: 3.5.11
106
+ rubygems_version: 3.3.27
172
107
  signing_key:
173
108
  specification_version: 4
174
109
  summary: Converter from Emacs Lisp to some document markup
data/.document DELETED
@@ -1,4 +0,0 @@
1
- README.md
2
- lib/**/*.rb
3
- CHANGELOG.md
4
- LICENSE.txt
data/.envrc DELETED
@@ -1,3 +0,0 @@
1
- watch_file manifest.scm
2
- export ELISP2ANY_GUIX_USE_PROFILE_PATH=yes
3
- use guix