jekyll-aplayer 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 "$@"