jekyll-aplayer 0.3.0

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.
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ module Jekyll::Aplayer
6
+ class Manager
7
+
8
+ @@_hooks = {}
9
+ @@_processors = []
10
+
11
+ def self.add(processor)
12
+ # register for listening event
13
+ processor.registers.each do |_register|
14
+ container = _register.first
15
+ events = _register.last.uniq
16
+ events = events.select do |event|
17
+ next true if event.match(/^post/)
18
+ next events.index(event.to_s.gsub(/^pre/, 'post').to_sym).nil?
19
+ end
20
+ events.each do |event|
21
+ self.hook container, event
22
+ end
23
+ end
24
+ @@_processors.push(processor)
25
+ @@_processors = @@_processors.sort { |a, b| b.priority <=> a.priority }
26
+ end
27
+
28
+ def self.hook(container, event, &block)
29
+ return if not is_hooked? container, event
30
+
31
+ handler = ->(page) {
32
+ self.dispatch page, container, event
33
+ block.call if block
34
+ }
35
+
36
+ if event.to_s.start_with?('after')
37
+ Jekyll::Hooks.register container, event do |page|
38
+ handler.call page
39
+ end
40
+ elsif event.to_s.start_with?('post')
41
+ Jekyll::Hooks.register container, event do |page|
42
+ handler.call page
43
+ end
44
+ # auto add pre-event
45
+ self.hook container, event.to_s.sub('post', 'pre').to_sym
46
+ elsif event.to_s.start_with?('pre')
47
+ Jekyll::Hooks.register container, event do |page|
48
+ handler.call page
49
+ end
50
+ end
51
+ end
52
+
53
+ def self.is_hooked?(container, event)
54
+ hook_name = "#{container}_#{event}".to_sym
55
+ return false if @@_hooks.has_key? hook_name
56
+ @@_hooks[hook_name] = true
57
+ end
58
+
59
+ def self.dispatch(page, container, event)
60
+ @@_processors.each do |processor|
61
+ processor.dispatch page, container, event
62
+ end
63
+ if event.to_s.start_with?('post') and Type.html? output_ext(page)
64
+ self.dispatch_html_block(page)
65
+ end
66
+ @@_processors.each do |processor|
67
+ processor.on_handled if processor.handled
68
+ end
69
+ end
70
+
71
+ def self.ext(page)
72
+ ext = page.path.match(/\.[^.]+$/)
73
+ ext.to_s.rstrip
74
+ end
75
+
76
+ def self.output_ext(page)
77
+ page.url_placeholders[:output_ext]
78
+ end
79
+
80
+ def self.converter(page, name)
81
+ page.site.converters.each do |converter|
82
+ class_name = converter.class.to_s.downcase
83
+ return converter if class_name.end_with?(name.downcase)
84
+ end
85
+ end
86
+
87
+ def self.dispatch_html_block(page)
88
+ doc = Nokogiri::HTML(page.output)
89
+ doc.css('script').each do |node|
90
+ type = Type.html_block_type node['type']
91
+ content = node.content
92
+ next if type.nil?
93
+
94
+ # dispatch to on_handle_html_block
95
+ @@_processors.each do |processor|
96
+ next unless processor.process?
97
+ content = processor.on_handle_html_block content, type
98
+ # dispatch to type handlers
99
+ method = "on_handle_#{type}"
100
+ next unless processor.respond_to? method
101
+ content = processor.pre_exclude content
102
+ content = processor.send method, content
103
+ content = processor.after_exclude content
104
+ end
105
+
106
+ cvter = self.converter page, type
107
+ content = cvter.convert content unless cvter.nil?
108
+
109
+ # dispatch to on_handle_html
110
+ @@_processors.each do |processor|
111
+ next unless processor.process?
112
+ content = processor.on_handle_html content
113
+ end
114
+ node.replace Nokogiri::HTML.fragment content
115
+ end
116
+ page.output = Processor.escape_html doc.to_html
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'deep_merge'
4
+ require 'securerandom'
5
+ require 'nokogiri'
6
+ require 'json/next'
7
+
8
+ module Jekyll::Aplayer
9
+ class Processor
10
+
11
+ DEFAULT_PRIORITY = 20
12
+
13
+ PRIORITY_MAP = {
14
+ :lowest => 0,
15
+ :low => 10,
16
+ :normal => 20,
17
+ :high => 30,
18
+ :highest => 40,
19
+ }.freeze
20
+
21
+ @@_registers = []
22
+ @@_exclusions = []
23
+ @@_priority = nil
24
+ @@_site_config = {}
25
+
26
+ attr_reader :page
27
+ attr_reader :logger
28
+ attr_reader :config
29
+ attr_reader :priority
30
+ attr_reader :registers
31
+ attr_reader :exclusions
32
+ attr_accessor :handled
33
+
34
+ def name
35
+ self.class.class_name
36
+ end
37
+
38
+ def self.class_name
39
+ self.name.split('::').last
40
+ end
41
+
42
+ def filename
43
+ self.name
44
+ .gsub(/([A-Z]+)([A-Z][a-z])/,'\1-\2')
45
+ .gsub(/([a-z\d])([A-Z])/,'\1-\2')
46
+ .tr("_", "-")
47
+ .downcase
48
+ end
49
+
50
+ def self.config
51
+ {}
52
+ end
53
+
54
+ def process?
55
+ return true if Type.html?(output_ext) or Type.markdown?(output_ext)
56
+ end
57
+
58
+ def ext
59
+ Manager.ext @page
60
+ end
61
+
62
+ def output_ext
63
+ Manager.output_ext @page
64
+ end
65
+
66
+ def initialize()
67
+ self.initialize_priority
68
+ self.initialize_register
69
+ self.initialize_exclusions
70
+ @logger = Logger.new(self.name)
71
+ @@_site_config = {}.deep_merge!(Config.site_config()).deep_merge!(self.config)
72
+ @handled_files = {}
73
+ end
74
+
75
+ ####
76
+ # Priority
77
+ #
78
+ def initialize_priority
79
+ @priority = @@_priority
80
+ unless @priority.nil? or @priority.is_a? Numeric
81
+ @priority = PRIORITY_MAP[@priority.to_sym]
82
+ end
83
+ @priority = DEFAULT_PRIORITY if @priority.nil?
84
+ @@_priority = nil
85
+ end
86
+
87
+ def self.priority(value)
88
+ @@_priority = value.to_sym
89
+ end
90
+
91
+ ####
92
+ # Register
93
+ #
94
+ def initialize_register
95
+ if @@_registers.size.zero?
96
+ self.class.register :pages, :pre_render, :post_render
97
+ self.class.register :documents, :pre_render, :post_render
98
+ end
99
+ @registers = Array.new @@_registers
100
+ @@_registers.clear
101
+ end
102
+
103
+ def self.register(container, *events)
104
+ @@_registers << [container, events]
105
+ end
106
+
107
+ ####
108
+ # Exclusions
109
+ #
110
+ def initialize_exclusions
111
+ if @@_exclusions.size.zero?
112
+ self.class.exclude :code, :math, :liquid_filter
113
+ end
114
+ @exclusions = @@_exclusions.uniq
115
+ @@_exclusions.clear
116
+ end
117
+
118
+ def self.exclude(*types)
119
+ @@_exclusions = types
120
+ end
121
+
122
+ def exclusion_regexs()
123
+ regexs = []
124
+ @exclusions.each do |type|
125
+ regex = nil
126
+ if type == :code
127
+ regex = /(((?<!\\)`{4,})\s*(\w*)((?:.|\n)*?)\2)/
128
+ elsif type == :math
129
+ regex = /(((?<!\\)\${1,2})[^\n]*?\1)/
130
+ elsif type == :liquid_filter
131
+ regex = /((?<!\\)((\{\{[^\n]*?\}\})|(\{%[^\n]*?%\})))/
132
+ end
133
+ regexs.push regex unless regex.nil?
134
+ end
135
+ regexs
136
+ end
137
+
138
+ def pre_exclude(content, regexs = self.exclusion_regexs())
139
+ @exclusion_store = []
140
+ regexs.each do |regex|
141
+ content.scan(regex) do |match_data|
142
+ match = match_data[0]
143
+ id = @exclusion_store.size
144
+ content = content.sub(match, "<!JEKYLL@#{object_id}@#{id}>")
145
+ @exclusion_store.push match
146
+ end
147
+ end
148
+ content
149
+ end
150
+
151
+ def post_exclude(content)
152
+ while @exclusion_store.size > 0
153
+ match = @exclusion_store.pop
154
+ id = @exclusion_store.size
155
+ content = content.sub("<!JEKYLL@#{object_id}@#{id}>", match)
156
+ end
157
+ @exclusion_store = []
158
+ content
159
+ end
160
+
161
+ def converter(name)
162
+ Manager.converter @page, name
163
+ end
164
+
165
+ def dispatch(page, container, event)
166
+ @page = page
167
+ @handled = false
168
+ return unless self.process?
169
+ method = "on_#{container}_#{event}"
170
+ self.send method, @page if self.respond_to? method
171
+ method = ''
172
+ if event.to_s.start_with?('pre')
173
+ if Type.markdown? ext
174
+ method = 'on_handle_markdown'
175
+ end
176
+ if self.respond_to? method
177
+ @page.content = self.pre_exclude @page.content
178
+ @page.content = self.send method, @page.content
179
+ @page.content = self.post_exclude @page.content
180
+ end
181
+ else
182
+ if Type.html? output_ext
183
+ method = 'on_handle_html'
184
+ elsif Type.css? output_ext
185
+ method = 'on_handle_css'
186
+ end
187
+ if self.respond_to? method
188
+ @page.output = self.send method, @page.output
189
+ if Type.html? output_ext
190
+ @page.output = self.class.escape_html(@page.output)
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+
197
+ ####
198
+ # Handle content.
199
+ #
200
+ def on_handle_markdown(content)
201
+ # Do not handle markdown if no aplayer placeholder.
202
+ return content unless Generator.has_aplayer_placeholder?(content, :code_aplayer)
203
+
204
+ # pre-handle aplayer code block in markdown.
205
+ content.scan(Generator.get_aplayer_regex(:code_aplayer)) do |match_data|
206
+ # Generate customize config.
207
+ config = {}
208
+ .deep_merge!(@@_site_config)
209
+ .deep_merge!(
210
+ HANSON.parse(match_data[3]).map {
211
+ |data|
212
+
213
+ # Convert string to real datatype.
214
+ method = 'to_' + Type.datatype?(data[0])
215
+ data[1] = data[1].send(method) if data[1].respond_to?(method)
216
+
217
+ data
218
+ }.to_h
219
+ )
220
+
221
+ # Skip if the processor not match.
222
+ next unless config['processor'].strip == self.name.downcase
223
+
224
+ # Replace aplayer placeholder.
225
+ uuid = config.key?('id') ? config['id'] : SecureRandom.uuid;
226
+
227
+ content = content.gsub(match_data[0], Nokogiri::HTML::Document.new.create_element('aplayer', '', {
228
+ 'id' => uuid,
229
+ 'class' => config['class'],
230
+ }).to_html)
231
+
232
+ # Store the each aplayers' config.
233
+ Config.store(uuid, config)
234
+ end
235
+
236
+ content
237
+ end
238
+
239
+ def on_handle_html_block(content, type)
240
+ # default handle method
241
+ content
242
+ end
243
+
244
+ def on_handle_html(content)
245
+ # Do not handle html if no aplayer placeholder.
246
+ return content unless Generator.has_aplayer_placeholder?(content, :html_aplayer)
247
+
248
+ # use nokogiri to parse html.
249
+ doc = Nokogiri::HTML(content)
250
+
251
+ # Prepare doms.
252
+ head = doc.at('head')
253
+ body = doc.at('body')
254
+ return content if head.nil? or body.nil?
255
+
256
+ # Inject assets into head.
257
+ @@_site_config['assets'].each do |type, value|
258
+ value.each do |asset|
259
+ Generator.asset_inject(:"#{type}", asset, head)
260
+ end
261
+ end
262
+
263
+ # Parse aplayer doc, and inset aplayer instances into body.
264
+ doc.css('aplayer').each do |elem|
265
+ elem['id'] = SecureRandom.uuid if !elem.key?('id') or elem['id'].empty?
266
+
267
+ config = {}.deep_merge!(
268
+ Config.get(elem['id']) ? Config.get(elem['id']) : @@_site_config
269
+ ).deep_merge!(
270
+ elem.keys.map { |key| [key, elem[key]] }.to_h
271
+ )
272
+
273
+ # Store each aplayers' config.
274
+ Config.store(elem['id'], config)
275
+
276
+ # Generate aplayer instance.
277
+ body.add_child(
278
+ Generator.generate_aplayer_instance(elem['id'], Config.normalize?(config))
279
+ ) if !body.to_html.include?(Generator.machine_id(elem['id']))
280
+ end
281
+
282
+ self.handled = true
283
+
284
+ doc.to_html
285
+ end
286
+
287
+ def on_handled
288
+ source = page.site.source
289
+ file = page.path.sub(/^#{source}\//, '')
290
+ return if @handled_files.has_key? file
291
+ @handled_files[file] = true
292
+ logger.log file
293
+ end
294
+
295
+ def self.escape_html(content)
296
+ # escape link
297
+ content.scan(/((https?:)?\/\/\S+\?[a-zA-Z0-9%\-_=\.&;]+)/) do |result|
298
+ result = result[0]
299
+ link = result.gsub('&amp;', '&')
300
+ content = content.gsub(result, link)
301
+ end
302
+ content
303
+ end
304
+
305
+ end
306
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll::Aplayer
4
+ class Register
5
+
6
+ def self.walk(start, &block)
7
+ Dir.foreach start do |x|
8
+ path = File.join(start, x)
9
+ if x == '.' or x == '..'
10
+ next
11
+ elsif File.directory?(path)
12
+ block.call(path + '/')
13
+ walk path
14
+ else
15
+ block.call(path)
16
+ end
17
+ end
18
+ end
19
+
20
+ def self.use(name)
21
+ name = name.to_s.gsub(/-/, '').downcase
22
+
23
+ self.walk(File.join(File.dirname(__FILE__), '/../processors')) do |path|
24
+ filename = File.basename(path, '.rb')
25
+ next if filename.gsub(/-/, '').downcase != name
26
+
27
+ Logger.log "🗂 use #{filename}"
28
+ require path
29
+ constants = Jekyll::Aplayer.constants.select do |c|
30
+ c.downcase.to_s == name
31
+ end
32
+
33
+ next if constants.first.nil?
34
+ _class = Jekyll::Aplayer.const_get(constants.first)
35
+ next unless _class.is_a? Class
36
+
37
+ Manager.add _class.new
38
+ end
39
+ end
40
+
41
+ def self.use_processors()
42
+ self.walk(File.join(File.dirname(__FILE__), '/../processors')) do |path|
43
+ filename = File.basename(path, '.rb')
44
+ self.use(filename)
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'string_to_boolean'
4
+
5
+ module Jekyll::Aplayer
6
+ class Type
7
+
8
+ PROPERTY_DATA_TYPES = {
9
+ 'processor' => 'string',
10
+ 'assets' => 'object',
11
+ 'id' => 'string',
12
+ 'class' => 'string',
13
+ 'fixed' => 'bool',
14
+ 'mini' => 'bool',
15
+ 'autoplay' => 'bool',
16
+ 'theme' => 'string',
17
+ 'loop' => 'string',
18
+ 'order' => 'string',
19
+ 'preload' => 'string',
20
+ 'volume' => 'float',
21
+ 'audio' => 'array',
22
+ 'mutex' => 'bool',
23
+ 'lrcType' => 'integer',
24
+ 'listFolded' => 'bool',
25
+ 'listMaxHeight' => 'integer',
26
+ 'storageName' => 'string',
27
+ }.freeze
28
+
29
+ HTML_EXTENSIONS = %w(
30
+ .html
31
+ .xhtml
32
+ .htm
33
+ ).freeze
34
+
35
+ CSS_EXTENSIONS = %w(
36
+ .css
37
+ .scss
38
+ ).freeze
39
+
40
+ MD_EXTENSIONS = %w(
41
+ .md
42
+ .markdown
43
+ ).freeze
44
+
45
+ HTML_BLOCK_TYPE_MAP = {
46
+ 'text/markdown' => 'markdown',
47
+ }.freeze
48
+
49
+ def self.html?(_ext)
50
+ HTML_EXTENSIONS.include?(_ext)
51
+ end
52
+
53
+ def self.css?(_ext)
54
+ CSS_EXTENSIONS.include?(_ext)
55
+ end
56
+
57
+ def self.markdown?(_ext)
58
+ MD_EXTENSIONS.include?(_ext)
59
+ end
60
+
61
+ def self.html_block_type(type)
62
+ HTML_BLOCK_TYPE_MAP[type]
63
+ end
64
+
65
+ def self.datatype?(property)
66
+ return PROPERTY_DATA_TYPES[property] if PROPERTY_DATA_TYPES.key?(property)
67
+ return 'string'
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll::Aplayer
4
+ class Util
5
+
6
+ def self.fetch_img_data(url)
7
+ begin
8
+ res = Net::HTTP.get_response URI(url)
9
+ raise res.body unless res.is_a?(Net::HTTPSuccess)
10
+ content_type = res.header['Content-Type']
11
+ raise 'Unknown content type!' if content_type.nil?
12
+ content_body = res.body.force_encoding('UTF-8')
13
+ return {
14
+ 'type' => content_type,
15
+ 'body' => content_body
16
+ }
17
+ rescue StandardError => msg
18
+ logger = Logger.new(self.class_name)
19
+ logger.log msg
20
+ end
21
+ end
22
+
23
+ def self.make_img_tag(data)
24
+ css_class = data['class']
25
+ type = data['type']
26
+ body = data['body']
27
+ if type == 'url'
28
+ "<img class=\"#{css_class}\" src=\"#{body}\">"
29
+ elsif type.include?('svg')
30
+ body.gsub(/\<\?xml.*?\?>/, '')
31
+ .gsub(/<!--[^\0]*?-->/, '')
32
+ .sub(/<svg /, "<svg class=\"#{css_class}\" ")
33
+ else
34
+ body = Base64.encode64(body)
35
+ body = "data:#{type};base64, #{body}"
36
+ "<img class=\"#{css_class}\" src=\"#{body}\">"
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll::Aplayer
4
+ class Default < Processor
5
+
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll::Aplayer
4
+ class Netease < Processor
5
+
6
+ def process?
7
+ return true if Type.html?(output_ext) or Type.markdown?(output_ext)
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Aplayer
5
+ VERSION = '0.3.0'
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jekyll-aplayer/cores/config'
4
+ require 'jekyll-aplayer/cores/extend'
5
+ require 'jekyll-aplayer/cores/generator'
6
+ require 'jekyll-aplayer/cores/logger'
7
+ require 'jekyll-aplayer/cores/manager'
8
+ require 'jekyll-aplayer/cores/processor'
9
+ require 'jekyll-aplayer/cores/register'
10
+ require 'jekyll-aplayer/cores/type'
11
+
12
+ module Jekyll::Aplayer
13
+ Logger::display_info
14
+ Config.load_config do
15
+ Register.use_processors
16
+ end
17
+ end
data/script/bootstrap ADDED
@@ -0,0 +1,4 @@
1
+ #! /bin/sh
2
+ set -ex
3
+
4
+ bundle install
data/script/cibuild ADDED
@@ -0,0 +1,4 @@
1
+ #! /bin/sh
2
+
3
+ script/fmt
4
+ script/test
data/script/fmt ADDED
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "Rubocop $(bundle exec rubocop --version)"
5
+ bundle exec rubocop -S -D -E $@
6
+ success=$?
7
+ if ((success != 0)); then
8
+ echo -e "\nTry running \`script/fmt -a\` to automatically fix errors"
9
+ fi
10
+ exit $success
data/script/release ADDED
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ set -ex
3
+
4
+ rake release
data/script/test ADDED
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ set -ex
3
+
4
+ bundle exec rspec "$@"