Restructure extensions with Ruby modules

Also restructured how they're named in the filesystem and the class
names as well.
This commit is contained in:
Gabriel Arazas 2023-11-06 22:54:57 +08:00
parent 69c8015292
commit de9f3a0e9c
No known key found for this signature in database
GPG Key ID: ADE0C41DAB221FCC
62 changed files with 823 additions and 764 deletions

View File

@ -1,3 +1,3 @@
# frozen_string_literal: true
require 'asciidoctor/foodogsquared-extensions'
require 'asciidoctor/foodogsquared/extensions'

View File

@ -1,61 +0,0 @@
# frozen_string_literal: true
class ChatBlock < Asciidoctor::Extensions::BlockProcessor
use_dsl
named :chat
on_context :example
name_positional_attributes 'avatar', 'state'
default_attributes 'state' => 'default'
def process(parent, reader, attrs)
block = create_block parent, :pass, nil, attrs, content_model: :compound
block.add_role('dialogblock')
# You can think of this section as a pipeline constructing the HTML
# component for this block. Specifically, we're building one component that
# contains two output: the dialog image of our avatar and its content.
attrs['name'] ||= attrs['avatar']
block << (create_html_fragment block, <<~HTML
<div role="figure" class="dialogblock dialogblock__box dialogblock__avatar--#{attrs['avatar']} #{attrs['role']}">
<div class="dialogblock dialogblock__avatar">
HTML
)
attrs['avatarsdir'] ||= File.expand_path('./avatars', attrs['iconsdir'])
attrs['avatarstype'] ||= parent.attributes['avatarstype'] || 'avif'
avatar_sticker = "#{attrs['avatar'].to_kebab}/#{attrs['state'].to_kebab}.#{attrs['avatarstype']}"
avatar_img_attrs = {
'target' => parent.image_uri(avatar_sticker, 'avatarsdir'),
'alt' => attrs['name']
}
avatar_imgblock = create_image_block block, avatar_img_attrs
block << avatar_imgblock
block << (create_html_fragment block, <<~HTML
</div>
<div class="dialogblock dialogblock__text">
<small class="dialogblock dialogblock__avatar-name">#{attrs['name']}</small>
HTML
)
parse_content block, reader
block << (create_html_fragment block, <<~HTML
</div>
</div>
HTML
)
block
end
private
def create_html_fragment(parent, html, attributes = nil)
create_block parent, :pass, html, attributes
end
end

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
require 'open-uri'
require 'yaml'
class FDroidLinkInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :fdroid
name_positional_attributes 'caption'
default_attributes 'lang' => 'en'
def process(parent, target, attrs)
doc = parent.document
app_id = target
app_metadata_uri = %(https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/#{app_id}.yml)
metadata = OpenURI.open_uri(app_metadata_uri) { |f| YAML.safe_load(f.read) }
attrs['caption'] ||= metadata['AutoName']
url = %(https://f-droid.org/#{attrs['lang']}/packages/#{app_id})
doc.register :links, url
create_anchor parent, attrs['caption'], type: :link, target: url
end
end

View File

@ -1,33 +0,0 @@
# frozen_string_literal: true
require 'json'
require 'open-uri'
class FlathubLinkInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :flathub
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
# FlatHub API seems to have no documentation aside from the source code.
# You can easily infer the API with its source code at
# https://github.com/flathub/website.
app_id = target
app_metadata_uri = %(https://flathub.org/api/v2/appstream/#{app_id})
headers = {
'Accept' => 'application/json',
'User-Agent' => ::Asciidoctor::FoodogsquaredCustomExtensions::USER_AGENT
}
metadata = OpenURI.open_uri(app_metadata_uri, headers) { |f| JSON.parse(f.read) }
attrs['caption'] ||= metadata['name']
url = %(https://flathub.org/apps/#{app_id})
doc.register :links, url
create_anchor parent, attrs['caption'], type: :link, target: url
end
end

View File

@ -1,55 +0,0 @@
# frozen_string_literal: true
require 'asciidoctor'
require 'asciidoctor/extensions'
require_relative 'helpers'
require_relative 'man-inline-macro/extension'
require_relative 'swhid-inline-macro/extension'
require_relative 'swhid-include-processor/extension'
require_relative 'github-link-inline-macro/extension'
require_relative 'github-raw-content-include-processor/extension'
require_relative 'gitlab-link-inline-macro/extension'
require_relative 'gitlab-raw-content-include-processor/extension'
require_relative 'chat-block-processor/extension'
require_relative 'git-blob-include-processor/extension'
require_relative 'wikipedia-inline-macro/extension'
require_relative 'package-indices-link-macro/extension'
require_relative 'fdroid-link-inline-macro/extension'
require_relative 'musicbrainz-link-inline-macro/extension'
require_relative 'flathub-link-inline-macro/extension'
require_relative 'repology-link-inline-macro/extension'
require_relative 'ietf-rfc-link-inline-macro/extension'
Asciidoctor::Extensions.register do
inline_macro ManInlineMacro
inline_macro IETFRFCLinkInlineMacro
block ChatBlock if @document.basebackend? 'html'
inline_macro SWHInlineMacro
include_processor SWHIDIncludeProcessor
inline_macro GitHubLinkInlineMacro
include_processor GitHubRawIncludeProcessor
inline_macro GitLabLinkInlineMacro
include_processor GitLabRawIncludeProcessor
include_processor GitBlobIncludeProcessor
inline_macro WikipediaInlineMacro
# Package indices
inline_macro CtanLinkInlineMacro
inline_macro PypiLinkInlineMacro
inline_macro CratesIOLinkInlineMacro
# App stores
inline_macro FDroidLinkInlineMacro
inline_macro FlathubLinkInlineMacro
# Databases
inline_macro MusicBrainzLinkInlineMacro
inline_macro RepologyLinkInlineMacro
end

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
module Asciidoctor::Foodogsquared::Extensions
class ChatBlock < Asciidoctor::Extensions::BlockProcessor
use_dsl
named :chat
on_context :example
name_positional_attributes 'avatar', 'state'
default_attributes 'state' => 'default'
def process(parent, reader, attrs)
# You can think of this section as a pipeline constructing the HTML
# component for this block. Specifically, we're building one component that
# contains two output: the dialog image of our avatar and its content.
attrs['name'] ||= attrs['avatar']
block = create_block parent, :pass, nil, attrs, content_model: :compound
block.add_role('dialogblock')
block << (create_html_fragment block, <<~HTML
<div role="figure" class="dialogblock dialogblock__box dialogblock__avatar--#{attrs['avatar']} #{attrs['role']}">
<div class="dialogblock dialogblock__avatar">
HTML
)
attrs['avatarsdir'] ||= File.expand_path('./avatars', attrs['iconsdir'])
attrs['avatarstype'] ||= parent.attributes['avatarstype'] || 'avif'
avatar_sticker = "#{attrs['avatar'].to_kebab}/#{attrs['state'].to_kebab}.#{attrs['avatarstype']}"
avatar_img_attrs = {
'target' => parent.image_uri(avatar_sticker, 'avatarsdir'),
'alt' => attrs['name']
}
avatar_imgblock = create_image_block block, avatar_img_attrs
block << avatar_imgblock
block << (create_html_fragment block, <<~HTML
</div>
<div class="dialogblock dialogblock__text">
<small class="dialogblock dialogblock__avatar-name">#{attrs['name']}</small>
HTML
)
parse_content block, reader
block << (create_html_fragment block, <<~HTML
</div>
</div>
HTML
)
block
end
private
def create_html_fragment(parent, html, attributes = nil)
create_block parent, :pass, html, attributes
end
end
end

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
require 'asciidoctor'
require 'asciidoctor/extensions'
require_relative 'helpers'
require_relative 'man-inline-macro/extension'
require_relative 'swhid-inline-macro/extension'
require_relative 'swhid-include-processor/extension'
require_relative 'github-inline-macro/extension'
require_relative 'github-include-processor/extension'
require_relative 'gitlab-inline-macro/extension'
require_relative 'gitlab-include-processor/extension'
require_relative 'chat-block/extension'
require_relative 'git-blob-include-processor/extension'
require_relative 'wikipedia-inline-macro/extension'
require_relative 'package-indices-macro/extension'
require_relative 'fdroid-inline-macro/extension'
require_relative 'musicbrainz-inline-macro/extension'
require_relative 'flathub-inline-macro/extension'
require_relative 'repology-inline-macro/extension'
require_relative 'ietf-rfc-inline-macro/extension'
include Asciidoctor::Foodogsquared::Extensions
Asciidoctor::Extensions.register do
inline_macro ManInlineMacro
inline_macro IETFRFCInlineMacro
block ChatBlock if @document.basebackend? 'html'
inline_macro SWHInlineMacro
include_processor SWHIncludeProcessor
inline_macro GitHubInlineMacro
include_processor GitHubIncludeProcessor
inline_macro GitLabInlineMacro
include_processor GitLabIncludeProcessor
include_processor GitBlobIncludeProcessor
inline_macro WikipediaInlineMacro
# Package indices
inline_macro CtanInlineMacro
inline_macro PypiInlineMacro
inline_macro CratesIOInlineMacro
# App stores
inline_macro FDroidInlineMacro
inline_macro FlathubInlineMacro
# Databases
inline_macro MusicBrainzInlineMacro
inline_macro RepologyInlineMacro
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'open-uri'
require 'yaml'
module Asciidoctor::Foodogsquared::Extensions
class FDroidInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :fdroid
name_positional_attributes 'caption'
default_attributes 'lang' => 'en'
def process(parent, target, attrs)
doc = parent.document
app_id = target
app_metadata_uri = %(https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/#{app_id}.yml)
metadata = OpenURI.open_uri(app_metadata_uri) { |f| YAML.safe_load(f.read) }
attrs['caption'] ||= metadata['AutoName']
url = %(https://f-droid.org/#{attrs['lang']}/packages/#{app_id})
doc.register :links, url
create_anchor parent, attrs['caption'], type: :link, target: url
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'json'
require 'open-uri'
module Asciidoctor::Foodogsquared::Extensions
class FlathubInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :flathub
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
# FlatHub API seems to have no documentation aside from the source code.
# You can easily infer the API with its source code at
# https://github.com/flathub/website.
app_id = target
app_metadata_uri = %(https://flathub.org/api/v2/appstream/#{app_id})
headers = {
'Accept' => 'application/json',
'User-Agent' => ::Asciidoctor::Foodogsquared::USER_AGENT
}
metadata = OpenURI.open_uri(app_metadata_uri, headers) { |f| JSON.parse(f.read) }
attrs['caption'] ||= metadata['name']
url = %(https://flathub.org/apps/#{app_id})
doc.register :links, url
create_anchor parent, attrs['caption'], type: :link, target: url
end
end
end

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'rugged'
module Asciidoctor::Foodogsquared::Extensions
class GitBlobIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor
def handles?(target)
target.start_with? 'git:'
end
def process(doc, reader, target, attrs)
attrs['gitrepo'] ||= doc.attributes['gitrepo'] || doc.base_dir
repo = Rugged::Repository.discover(attrs['gitrepo'])
git_object_ref = target.delete_prefix 'git:'
git_object_ref = doc.attributes['doccontentref'] if git_object_ref.empty?
begin
git_object = repo.rev_parse git_object_ref
if attrs.key? 'diff-option'
options = {}
options[:paths] = attrs['path'].split(';') if attrs.key? 'path'
options[:context_lines] = attrs['context-lines'] if attrs.key? 'context-lines'
options[:reverse] = true if attrs.key? 'reverse-option'
if attrs.key? 'other'
other = repo.rev_parse attrs['other'] || nil
reader.push_include git_object.diff(other, **options).patch
else
reader.push_include git_object.diff(**options).patch
end
else
inner_entry = case git_object.type
when :blob
git_object
when :commit
git_object.tree.path attrs['path']
when :tree
git_object.path attrs['path']
when :tag
git_object.target.tree.path attrs['path']
end
content = repo.lookup(inner_entry[:oid]).content
if attrs.key? 'lines'
content_lines = content.lines
new_content = +''
doc.resolve_lines_to_highlight(content, attrs['lines']).each do |line_no|
new_content << content_lines.at(line_no - 1)
end
content = new_content
end
reader.push_include content
end
rescue StandardError => e
reader.push_include "Unresolved directive for '#{target}' with the following error:\n#{e}"
warn e
end
reader
end
end
end

View File

@ -0,0 +1,70 @@
# frozen_string_literal: true
require 'base64'
require 'json'
require 'open-uri'
require 'uri'
module Asciidoctor::Foodogsquared::Extensions
class GitHubIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor
def handles?(target)
target.start_with? 'github:'
end
def warn_or_raise(doc, warning)
if (doc.safe > Asciidoctor::SafeMode::SERVER) && !(doc.attr? 'allow-uri-read')
raise warning
else
warn warning
end
end
def process(doc, reader, target, attrs)
src = target.delete_prefix('github:').split('/', 3)
owner = src.at 0
repo = src.at 1
namespaced_repo = "#{owner}/#{repo}"
path = attrs['path'] || ''
# For more information, see https://docs.github.com/en/rest/repos/contents.
uri = URI.parse %(https://api.github.com/repos/#{owner}/#{repo}/contents/#{path})
if attrs['rev']
query = { ref: attrs['rev'] }
uri.query = URI.encode_www_form query
end
begin
headers = {
'Header' => 'application/vnd.github+json',
'X-GitHub-Api-Version' => '2022-11-28'
}
headers['Authorization'] = "Token #{ENV['GITHUB_API_BEARER_TOKEN']}" if ENV['GITHUB_API_BEARER_TOKEN']
OpenURI.open_uri(uri, headers) do |f|
response = JSON.parse(f.read)
# If the response is an array, it is likely to be a directory. In this
# usecase, we'll just list them.
content = if response.is_a? Array
warning = %(given path '#{path}' from GitHub repo '#{repo}' is a directory)
warn_or_raise doc, warning
warning
elsif response.is_a? Object
Base64.decode64 response['content'] if response['content'] && response['encoding'] == 'base64'
end
reader.push_include content, target, target, 1, attrs
end
rescue OpenURI::HTTPError => e
warning = %(error while getting '#{path}' in GitHub repo '#{namespaced_repo}: #{e}')
warn_or_raise doc, warning
reader.push_include warning, target, target, 1, attrs
end
reader
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'uri'
module Asciidoctor::Foodogsquared::Extensions
class GitHubInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :github
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
default_caption = if attrs.key?('repo-option')
target.split('/').at(1)
else
target
end
text = attrs['caption'] || default_caption
uri = URI.parse %(https://github.com/#{target})
if attrs.key? 'issue'
uri.path += %(/issues/#{attrs['issue']})
text << "##{attrs['issue']}" if text == target
else
uri.path += %(/tree/#{attrs['rev']}) if attrs.key? 'rev'
uri.path += %(/#{attrs['path']}) if attrs.key? 'path'
text << "@#{attrs['rev']}" if attrs.key?('rev') && text == target
end
target = uri.to_s
doc.register :links, target
create_anchor parent, text, type: :link, target: target
end
end
end

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
require 'base64'
require 'json'
require 'open-uri'
require 'uri'
module Asciidoctor::Foodogsquared::Extensions
class GitLabIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor
def handles?(target)
target.start_with? 'gitlab:'
end
def process(doc, reader, target, attrs)
src = target.delete_prefix('gitlab:').split('/', 2)
owner = src.at 0
repo = src.at 1
namespaced_repo = "#{owner}/#{repo}"
raise %(there is no 'path' attribute given for GitLab repo '#{namespaced_repo}') unless attrs.key? 'path'
raise %(no given ref for getting file in '#{namespaced_repo}') unless attrs.key? 'rev'
path = attrs['path']
rev = attrs['rev']
domain = attrs['domain'] || 'gitlab.com'
version = attrs['version'] || 'v4'
uri = URI.parse %(https://#{domain}/api/#{version})
# Set the project.
uri += %(/projects/#{URI.encode_www_form_component namespaced_repo})
# Then the filename.
uri += %(/repository/files/#{URI.encode_www_form_component path})
# Then the revision.
query = { ref: rev }
uri.query = URI.encode_www_form query
content = begin
headers = { 'Content-Type' => 'application-json' }
header['PRIVATE-TOKEN'] = ENV['GITLAB_API_PERSONAL_ACCESS_TOKEN'] if ENV['GITLAB_API_PERSONAL_ACCESS_TOKEN']
OpenURI.open_uri(uri, headers) do |f|
response = JSON.parse(f.read)
Base64.decode64 response['content'] if response['content'] && response['encoding'] == 'base64'
end
rescue OpenURI::HTTPError => e
warning = %(error while getting '#{path}' in GitLab repo '#{repo}': #{e})
warn_or_raise doc, warning
warning
end
reader.push_include content, target, target, 1, attrs
reader
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'uri'
module Asciidoctor::Foodogsquared::Extensions
class GitLabInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :gitlab
name_positional_attributes 'caption'
default_attributes 'domain' => 'gitlab.com'
def process(parent, target, attrs)
doc = parent.document
default_caption = if attrs.key?('repo-option')
target.split('/').at(1)
else
target
end
text = attrs['caption'] || default_caption
uri = URI.parse %(https://#{attrs['domain']}/#{target})
if attrs.key? 'issue'
uri.path += %(/-/issues/#{attrs['issue']})
text << "##{attrs['issue']}" if text == target
else
uri.path += %(/-/tree/#{attrs['rev']}) if attrs.key? 'rev'
uri.path += %(/#{attrs['path']}) if attrs.key? 'path'
text << "@#{attrs['rev']}" if attrs.key?('rev') && text == target
end
target = uri.to_s
doc.register :links, target
create_anchor parent, text, type: :link, target: target
end
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class String
def to_kebab
self.gsub(/\s+/, '-') # Replace all spaces with dashes.
.gsub(/[^a-zA-Z0-9-]/, '') # Remove all non-alphanumerical (and dashes) characters.
.gsub(/-+/, '-') # Reduce all dashes into only one.
.gsub(/^-|-+$/, '') # Remove all leading and trailing dashes.
.downcase
end
end
# The namespace for storing Asciidoctor. This is the entry point for the
# entirety of this project.
module Asciidoctor::Foodogsquared
NAME = 'asciidoctor-foodogsquared-custom-extensions'
VERSION = '1.2.0'
CONTACT_EMAIL = 'foodogsquared@foodogsquared.one'
USER_AGENT = "#{NAME}/#{VERSION} ( #{CONTACT_EMAIL} )".freeze
def warn_or_raise(doc, warning)
return raise warning if (doc.safe > Asciidoctor::SafeMode::SERVER) && !(doc.attr? 'allow-uri-read')
warn warning
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Asciidoctor::Foodogsquared::Extensions
class IETFRFCInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :rfc
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
url = %(https://datatracker.ietf.org/doc/html/#{target})
attrs['caption'] ||= "RFC#{target}"
doc.register :links, url
create_anchor parent, attrs['caption'], type: :link, target: url
end
end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
module Asciidoctor::Foodogsquared::Extensions
class ManInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :man
name_positional_attributes 'volnum'
default_attributes 'service' => 'debian', 'subpath' => ''
def process(parent, target, attrs)
doc = parent.document
manname = target
text = %(#{manname}(#{attrs['volnum']}))
if doc.basebackend? 'html'
domain = case attrs['service']
when 'debian'
'https://manpages.debian.org'
when 'ubuntu'
'https://manpages.ubuntu.org'
when 'arch'
'https://man.archlinux.org/man'
when 'opensuse'
'https://manpages.opensuse.org'
when 'voidlinux'
'https://man.voidlinux.org'
when 'openbsd'
'https://man.openbsd.org'
when 'none'
nil
else
raise "no available manpage service #{attrs['service']}"
end
if !domain.nil?
target = %(#{domain}/#{attrs['subpath'].delete_prefix '/'}#{manname}.#{attrs['volnum']})
doc.register :links, target
node = create_anchor parent, text, type: :link, target: target
else
node = create_inline parent, :quoted, text
end
elsif doc.backend == 'manpage'
node = create_inline parent, :quoted, text, type: :strong
else
node = create_inline parent, :quoted, text
end
node
end
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'json'
require 'open-uri'
require 'uri'
module Asciidoctor::Foodogsquared::Extensions
class MusicBrainzInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :musicbrainz
name_positional_attributes 'caption', 'type'
default_attributes 'type' => 'release'
def process(parent, target, attrs)
doc = parent.document
root_endpoint = 'https://musicbrainz.org/ws/2'
begin
headers = {
'Accept' => 'application/json',
'User-Agent' => ::Asciidoctor::Foodogsquared::USER_AGENT
}
uri = %(#{root_endpoint}/#{attrs['type']}/#{target})
metadata = OpenURI.open_uri(uri, headers) { |f| JSON.parse(f.read) }
attrs['caption'] ||= case attrs['type']
when 'artist', 'area', 'events', 'genre', 'instrument', 'label', 'place', 'series'
metadata['name']
when 'recording', 'release-group', 'release', 'cdstub', 'work'
metadata['title']
when 'url'
metadata['resource']
end
target = %(https://musicbrainz.org/#{attrs['type']}/#{target})
doc.register :links, target
create_anchor parent, attrs['caption'], type: :link, target: target
rescue StandardError
warning = %(error while getting Musicbrainz database object '#{target}: #{e}')
warn_or_raise doc, warning
reader.push_include warning, target, target, 1, attrs
end
end
end
end

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
# I'm fairly sure this could be programmed since Ruby has nice metaprogramming
# capabilities. Though, we'll be keeping it manually defining classes for now
# for initial versions since there could be additional features for each macro.
module Asciidoctor::Foodogsquared::Extensions
class CtanInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :ctan
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
text = attrs['caption'] || target
url = %(https://ctan.org/pkg/#{target})
doc.register :links, url
create_anchor parent, text, type: :link, target: url
end
end
class PypiInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :pypi
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
text = attrs['caption'] || target
url = %(https://pypi.org/project/#{target})
doc.register :links, url
create_anchor parent, text, type: :link, target: url
end
end
class CratesIOInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :cratesio
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
text = attrs['caption'] || target
url = %(https://crates.io/crates/#{target})
doc.register :links, url
create_anchor parent, text, type: :link, target: url
end
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Asciidoctor::Foodogsquared::Extensions
class RepologyInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :repology
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
text = attrs['caption'] || target
url = %(https://repology.org/project/#{target})
doc.register :links, url
create_anchor parent, text, type: :link, target: url
end
end
end

View File

@ -0,0 +1,26 @@
= SPDX link inline macro
:toc:
It's an inline macro for easily creating licenses from SPDX license data.
== Synopsis
[source, asciidoc]
----
spdx:$LICENSE_ID[$CAPTION]
----
Where...
* `$LICENSE_ID` is the identifier for the license.
* `$CAPTION` is the link caption to be used.
By default, it will use the name of the license.
== Example usage
* `spdx:MIT[]` will result to the link:https://spdx.org/licenses/MIT.html[MIT license page on SPDX] with the caption `MIT License`.
* `spdx:MIT[the MIT license]` is the same as the previous example but with a different caption of `the MIT license`.

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'json'
require 'open-uri'
require 'uri'
module Asciidoctor::Foodogsquared::Extensions
class SWHIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor
def handles?(target)
target.start_with? 'swh:'
end
def process(doc, reader, target, attributes)
swhid = target
swhid_core_identifier = swhid.split(';').at(0)
swhid_object_type = (swhid_core_identifier.split ':').at 2
unless (doc.safe <= Asciidoctor::SafeMode::SERVER) && (doc.attr? 'allow-uri-read')
raise %('swh:' include cannot be used in safe mode level > SERVER and without attribute 'allow-uri-read')
end
# We're already going to throw out anything that is not content object type
# just to make the later pipelines easier to construct.
if swhid_object_type != 'cnt'
warn %(SWHID '#{swhid_core_identifier}' is not of 'cnt' type; ignoring)
return reader
end
version = '1'
content = begin
uri = URI.parse %(https://archive.softwareheritage.org/api/#{version}/resolve/#{target}/)
headers = {
'Accept' => 'application/json'
}
headers['Authorization'] = "Bearer #{ENV['SWH_API_BEARER_TOKEN']}" if ENV['SWH_API_BEARER_TOKEN']
metadata = OpenURI.open_uri(uri, headers) { |f| JSON.parse(f.read) }
object_hash = metadata['object_id']
uri = URI.parse %(https://archive.softwareheritage.org/api/#{version}/content/sha1_git:#{object_hash}/raw/)
OpenURI.open_uri(uri, headers, &:read)
rescue OpenURI::HTTPError => e
warning = %(error while getting '#{swhid_core_identifier}': #{e})
warn warning
warning
end
reader.push_include content, target, target, 1, attributes
reader
end
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
module Asciidoctor::Foodogsquared::Extensions
class SWHInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :swh
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
# We're only considering `swh:` starting with the scheme version. Also, it
# looks nice aesthetically.
swhid = target.start_with?('swh:') ? target : %(swh:#{target})
default_caption = if attrs.key? 'full-option'
swhid
else
swhid.split(';').at(0)
end
text = attrs['caption'] || default_caption
target = %(https://archive.softwareheritage.org/#{swhid})
doc.register :links, target
create_anchor parent, text, type: :link, target: target
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'uri'
module Asciidoctor::Foodogsquared::Extensions
class WikipediaInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :wikipedia
name_positional_attributes 'caption'
default_attributes 'lang' => 'en'
def process(parent, target, attrs)
caption = attrs['caption'] || target
parser = URI::Parser.new
page = parser.escape target
link = %(https://#{attrs['lang']}.wikipedia.org/wiki/#{page})
node = create_anchor parent, caption, type: :link, target: link
create_inline parent, :quoted, node.convert
end
end
end

View File

@ -1,66 +0,0 @@
# frozen_string_literal: true
require 'rugged'
class GitBlobIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor
def handles?(target)
target.start_with? 'git:'
end
def process(doc, reader, target, attrs)
attrs['gitrepo'] ||= doc.attributes['gitrepo'] || doc.base_dir
repo = Rugged::Repository.discover(attrs['gitrepo'])
git_object_ref = target.delete_prefix 'git:'
git_object_ref = doc.attributes['doccontentref'] if git_object_ref.empty?
begin
git_object = repo.rev_parse git_object_ref
if attrs.key? 'diff-option'
options = {}
options[:paths] = attrs['path'].split(';') if attrs.key? 'path'
options[:context_lines] = attrs['context-lines'] if attrs.key? 'context-lines'
options[:reverse] = true if attrs.key? 'reverse-option'
if attrs.key? 'other'
other = repo.rev_parse attrs['other'] || nil
reader.push_include git_object.diff(other, **options).patch
else
reader.push_include git_object.diff(**options).patch
end
else
inner_entry = case git_object.type
when :blob
git_object
when :commit
git_object.tree.path attrs['path']
when :tree
git_object.path attrs['path']
when :tag
git_object.target.tree.path attrs['path']
end
content = repo.lookup(inner_entry[:oid]).content
if attrs.key? 'lines'
content_lines = content.lines
new_content = +''
doc.resolve_lines_to_highlight(content, attrs['lines']).each do |line_no|
new_content << content_lines.at(line_no - 1)
end
content = new_content
end
reader.push_include content
end
rescue StandardError => e
reader.push_include "Unresolved directive for '#{target}' with the following error:\n#{e}"
warn e
end
reader
end
end

View File

@ -1,36 +0,0 @@
# frozen_string_literal: true
require 'uri'
class GitHubLinkInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :github
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
default_caption = if attrs.key?('repo-option')
target.split('/').at(1)
else
target
end
text = attrs['caption'] || default_caption
uri = URI.parse %(https://github.com/#{target})
if attrs.key? 'issue'
uri.path += %(/issues/#{attrs['issue']})
text << "##{attrs['issue']}" if text == target
else
uri.path += %(/tree/#{attrs['rev']}) if attrs.key? 'rev'
uri.path += %(/#{attrs['path']}) if attrs.key? 'path'
text << "@#{attrs['rev']}" if attrs.key?('rev') && text == target
end
target = uri.to_s
doc.register :links, target
create_anchor parent, text, type: :link, target: target
end
end

View File

@ -1,68 +0,0 @@
# frozen_string_literal: true
require 'base64'
require 'json'
require 'open-uri'
require 'uri'
class GitHubRawIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor
def handles?(target)
target.start_with? 'github:'
end
def warn_or_raise(doc, warning)
if (doc.safe > Asciidoctor::SafeMode::SERVER) && !(doc.attr? 'allow-uri-read')
raise warning
else
warn warning
end
end
def process(doc, reader, target, attrs)
src = target.delete_prefix('github:').split('/', 3)
owner = src.at 0
repo = src.at 1
namespaced_repo = "#{owner}/#{repo}"
path = attrs['path'] || ''
# For more information, see https://docs.github.com/en/rest/repos/contents.
uri = URI.parse %(https://api.github.com/repos/#{owner}/#{repo}/contents/#{path})
if attrs['rev']
query = { ref: attrs['rev'] }
uri.query = URI.encode_www_form query
end
begin
headers = {
'Header' => 'application/vnd.github+json',
'X-GitHub-Api-Version' => '2022-11-28'
}
headers['Authorization'] = "Token #{ENV['GITHUB_API_BEARER_TOKEN']}" if ENV['GITHUB_API_BEARER_TOKEN']
OpenURI.open_uri(uri, headers) do |f|
response = JSON.parse(f.read)
# If the response is an array, it is likely to be a directory. In this
# usecase, we'll just list them.
content = if response.is_a? Array
warning = %(given path '#{path}' from GitHub repo '#{repo}' is a directory)
warn_or_raise doc, warning
warning
elsif response.is_a? Object
Base64.decode64 response['content'] if response['content'] && response['encoding'] == 'base64'
end
reader.push_include content, target, target, 1, attrs
end
rescue OpenURI::HTTPError => e
warning = %(error while getting '#{path}' in GitHub repo '#{namespaced_repo}: #{e}')
warn_or_raise doc, warning
reader.push_include warning, target, target, 1, attrs
end
reader
end
end

View File

@ -1,37 +0,0 @@
# frozen_string_literal: true
require 'uri'
class GitLabLinkInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :gitlab
name_positional_attributes 'caption'
default_attributes 'domain' => 'gitlab.com'
def process(parent, target, attrs)
doc = parent.document
default_caption = if attrs.key?('repo-option')
target.split('/').at(1)
else
target
end
text = attrs['caption'] || default_caption
uri = URI.parse %(https://#{attrs['domain']}/#{target})
if attrs.key? 'issue'
uri.path += %(/-/issues/#{attrs['issue']})
text << "##{attrs['issue']}" if text == target
else
uri.path += %(/-/tree/#{attrs['rev']}) if attrs.key? 'rev'
uri.path += %(/#{attrs['path']}) if attrs.key? 'path'
text << "@#{attrs['rev']}" if attrs.key?('rev') && text == target
end
target = uri.to_s
doc.register :links, target
create_anchor parent, text, type: :link, target: target
end
end

View File

@ -1,66 +0,0 @@
# frozen_string_literal: true
require 'base64'
require 'json'
require 'open-uri'
require 'uri'
class GitLabRawIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor
def handles?(target)
target.start_with? 'gitlab:'
end
def warn_or_raise(doc, warning)
if (doc.safe > Asciidoctor::SafeMode::SERVER) && !(doc.attr? 'allow-uri-read')
raise warning
else
warn warning
end
end
def process(doc, reader, target, attrs)
src = target.delete_prefix('gitlab:').split('/', 2)
owner = src.at 0
repo = src.at 1
namespaced_repo = "#{owner}/#{repo}"
raise %(there is no 'path' attribute given for GitLab repo '#{namespaced_repo}') unless attrs.key? 'path'
raise %(no given ref for getting file in '#{namespaced_repo}') unless attrs.key? 'rev'
path = attrs['path']
rev = attrs['rev']
domain = attrs['domain'] || 'gitlab.com'
version = attrs['version'] || 'v4'
uri = URI.parse %(https://#{domain}/api/#{version})
# Set the project.
uri += %(/projects/#{URI.encode_www_form_component namespaced_repo})
# Then the filename.
uri += %(/repository/files/#{URI.encode_www_form_component path})
# Then the revision.
query = { ref: rev }
uri.query = URI.encode_www_form query
content = begin
headers = { 'Content-Type' => 'application-json' }
header['PRIVATE-TOKEN'] = ENV['GITLAB_API_PERSONAL_ACCESS_TOKEN'] if ENV['GITLAB_API_PERSONAL_ACCESS_TOKEN']
OpenURI.open_uri(uri, headers) do |f|
response = JSON.parse(f.read)
Base64.decode64 response['content'] if response['content'] && response['encoding'] == 'base64'
end
rescue OpenURI::HTTPError => e
warning = %(error while getting '#{path}' in GitLab repo '#{repo}': #{e})
warn_or_raise doc, warning
warning
end
reader.push_include content, target, target, 1, attrs
reader
end
end

View File

@ -1,20 +0,0 @@
# frozen_string_literal: true
class String
def to_kebab
self.gsub(/\s+/, '-') # Replace all spaces with dashes.
.gsub(/[^a-zA-Z0-9-]/, '') # Remove all non-alphanumerical (and dashes) characters.
.gsub(/-+/, '-') # Reduce all dashes into only one.
.gsub(/^-|-+$/, '') # Remove all leading and trailing dashes.
.downcase
end
end
module Asciidoctor
module FoodogsquaredCustomExtensions
NAME = 'asciidoctor-foodogsquared-custom-extensions'
VERSION = '1.0.0'
CONTACT_EMAIL = 'foodogsquared@foodogsquared.one'
USER_AGENT = "#{NAME}/#{VERSION} ( #{CONTACT_EMAIL} )"
end
end

View File

@ -1,16 +0,0 @@
# frozen_string_literal: true
class IETFRFCLinkInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :rfc
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
url = %(https://datatracker.ietf.org/doc/html/#{target})
attrs['caption'] ||= "RFC#{target}"
doc.register :links, url
create_anchor parent, attrs['caption'], type: :link, target: url
end
end

View File

@ -1,51 +0,0 @@
# frozen_string_literal: true
class ManInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :man
name_positional_attributes 'volnum'
default_attributes 'service' => 'debian', 'subpath' => ''
def process(parent, target, attrs)
doc = parent.document
manname = target
text = %(#{manname}(#{attrs['volnum']}))
if doc.basebackend? 'html'
domain = case attrs['service']
when 'debian'
'https://manpages.debian.org'
when 'ubuntu'
'https://manpages.ubuntu.org'
when 'arch'
'https://man.archlinux.org/man'
when 'opensuse'
'https://manpages.opensuse.org'
when 'voidlinux'
'https://man.voidlinux.org'
when 'openbsd'
'https://man.openbsd.org'
when 'none'
nil
else
raise "no available manpage service #{attrs['service']}"
end
if !domain.nil?
target = %(#{domain}/#{attrs['subpath'].delete_prefix '/'}#{manname}.#{attrs['volnum']})
doc.register :links, target
node = create_anchor parent, text, type: :link, target: target
else
node = create_inline parent, :quoted, text
end
elsif doc.backend == 'manpage'
node = create_inline parent, :quoted, text, type: :strong
else
node = create_inline parent, :quoted, text
end
node
end
end

View File

@ -1,45 +0,0 @@
# frozen_string_literal: true
require 'json'
require 'open-uri'
require 'uri'
class MusicBrainzLinkInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :musicbrainz
name_positional_attributes 'caption', 'type'
default_attributes 'type' => 'release'
def process(parent, target, attrs)
doc = parent.document
root_endpoint = 'https://musicbrainz.org/ws/2'
begin
headers = {
'Accept' => 'application/json',
'User-Agent' => ::Asciidoctor::FoodogsquaredCustomExtensions::USER_AGENT
}
uri = %(#{root_endpoint}/#{attrs['type']}/#{target})
metadata = OpenURI.open_uri(uri, headers) { |f| JSON.parse(f.read) }
attrs['caption'] ||= case attrs['type']
when 'artist', 'area', 'events', 'genre', 'instrument', 'label', 'place', 'series'
metadata['name']
when 'recording', 'release-group', 'release', 'cdstub', 'work'
metadata['title']
when 'url'
metadata['resource']
end
target = %(https://musicbrainz.org/#{attrs['type']}/#{target})
doc.register :links, target
create_anchor parent, attrs['caption'], type: :link, target: target
rescue
warning = %(error while getting Musicbrainz database object '#{target}: #{e}')
warn_or_raise doc, warning
reader.push_include warning, target, target, 1, attrs
end
end
end

View File

@ -1,56 +0,0 @@
# frozen_string_literal: true
# I'm fairly sure this could be programmed since Ruby has nice metaprogramming
# capabilities. Though, we'll be keeping it manually defining classes for now
# for initial versions since there could be additional features for each macro.
class CtanLinkInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :ctan
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
text = attrs['caption'] || target
url = %(https://ctan.org/pkg/#{target})
doc.register :links, url
create_anchor parent, text, type: :link, target: url
end
end
class PypiLinkInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :pypi
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
text = attrs['caption'] || target
url = %(https://pypi.org/project/#{target})
doc.register :links, url
create_anchor parent, text, type: :link, target: url
end
end
class CratesIOLinkInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :cratesio
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
text = attrs['caption'] || target
url = %(https://crates.io/crates/#{target})
doc.register :links, url
create_anchor parent, text, type: :link, target: url
end
end

View File

@ -1,18 +0,0 @@
# frozen_string_literal: true
class RepologyLinkInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :repology
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
text = attrs['caption'] || target
url = %(https://repology.org/project/#{target})
doc.register :links, url
create_anchor parent, text, type: :link, target: url
end
end

View File

@ -1,53 +0,0 @@
# frozen_string_literal: true
require 'json'
require 'open-uri'
require 'uri'
class SWHIDIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor
def handles?(target)
target.start_with? 'swh:'
end
def process(doc, reader, target, attributes)
swhid = target
swhid_core_identifier = swhid.split(';').at(0)
swhid_object_type = (swhid_core_identifier.split ':').at 2
unless (doc.safe <= Asciidoctor::SafeMode::SERVER) && (doc.attr? 'allow-uri-read')
raise %('swh:' include cannot be used in safe mode level > SERVER and without attribute 'allow-uri-read')
end
# We're already going to throw out anything that is not content object type
# just to make the later pipelines easier to construct.
if swhid_object_type != 'cnt'
warn %(SWHID '#{swhid_core_identifier}' is not of 'cnt' type; ignoring)
return reader
end
version = '1'
content = begin
uri = URI.parse %(https://archive.softwareheritage.org/api/#{version}/resolve/#{target}/)
headers = {
'Accept' => 'application/json'
}
headers['Authorization'] = "Bearer #{ENV['SWH_API_BEARER_TOKEN']}" if ENV['SWH_API_BEARER_TOKEN']
metadata = OpenURI.open_uri(uri, headers) { |f| JSON.parse(f.read) }
object_hash = metadata['object_id']
uri = URI.parse %(https://archive.softwareheritage.org/api/#{version}/content/sha1_git:#{object_hash}/raw/)
OpenURI.open_uri(uri, headers, &:read)
rescue OpenURI::HTTPError => e
warning = %(error while getting '#{swhid_core_identifier}': #{e})
warn warning
warning
end
reader.push_include content, target, target, 1, attributes
reader
end
end

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
class SWHInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :swh
name_positional_attributes 'caption'
def process(parent, target, attrs)
doc = parent.document
# We're only considering `swh:` starting with the scheme version. Also, it
# looks nice aesthetically.
swhid = target.start_with?('swh:') ? target : %(swh:#{target})
default_caption = if attrs.key? 'full-option'
swhid
else
swhid.split(';').at(0)
end
text = attrs['caption'] || default_caption
target = %(https://archive.softwareheritage.org/#{swhid})
doc.register :links, target
create_anchor parent, text, type: :link, target: target
end
end

View File

@ -1,21 +0,0 @@
# frozen_string_literal: true
require 'uri'
class WikipediaInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor
use_dsl
named :wikipedia
name_positional_attributes 'caption'
default_attributes 'lang' => 'en'
def process(parent, target, attrs)
caption = attrs['caption'] || target
parser = URI::Parser.new
page = parser.escape target
link = %(https://#{attrs['lang']}.wikipedia.org/wiki/#{page})
node = create_anchor parent, caption, type: :link, target: link
create_inline parent, :quoted, node.convert
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
describe FDroidLinkInlineMacro do
describe FDroidInlineMacro do
it 'should create a FDroid link' do
input = 'fdroid:org.moire.ultrasonic[]'

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
describe FlathubLinkInlineMacro do
describe FlathubInlineMacro do
it 'should create a Flathub link to Icon Library app' do
input = 'flathub:org.gnome.design.IconLibrary[]'

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
describe GitHubLinkInlineMacro do
describe GitHubInlineMacro do
it 'should create a GitHub link with the caption being the target' do
input = <<~INPUT
github:foo-dogsquared/foobarbazxyz[]

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
describe GitHubRawIncludeProcessor do
describe GitHubIncludeProcessor do
it 'should include the raw content of a GitHub file successfully' do
input = <<~INPUT
[literal]

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
describe GitLabLinkInlineMacro do
describe GitLabInlineMacro do
it 'should link to the GitLab page for GitLab project' do
input = 'gitlab:gitlab-org/gitlab[]'

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
describe GitLabRawIncludeProcessor, if: ENV['GITLAB_API_PERSONAL_ACCESS_TOKEN'] do
describe GitLabIncludeProcessor, if: ENV['GITLAB_API_PERSONAL_ACCESS_TOKEN'] do
it 'should include the GitLab CI configuration from freedesktop-sdk/freedesktop-sdk from the default instance' do
commit = 'bcb3e0de957519e87a4c7b8c0e40af9876e531e7'
input = <<~INPUT

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
describe MusicBrainzLinkInlineMacro do
describe MusicBrainzInlineMacro do
it 'should create a MusicBrainz release object with the right captions' do
input = 'musicbrainz:9adcff14-7dba-4ccf-a6a6-298bcde3dd46[]'

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
describe RepologyLinkInlineMacro do
describe RepologyInlineMacro do
it 'should link to the Repology page for beets' do
input = 'repology:beets[]'

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'asciidoctor'
require 'asciidoctor/foodogsquared-extensions'
require 'asciidoctor-foodogsquared-extensions'
include Asciidoctor::Foodogsquared::Extensions
RSpec.configure do
def fixtures_dir
File.join __dir__, 'fixtures'