mirror of
https://github.com/google/pebble.git
synced 2025-03-15 08:41:21 +00:00
323 lines
11 KiB
Ruby
323 lines
11 KiB
Ruby
# Copyright 2025 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
require 'redcarpet'
|
|
require 'pygments'
|
|
require 'slugize'
|
|
require 'nokogiri'
|
|
|
|
module Jekyll
|
|
module Converters
|
|
# Jekyll Markdown Converter wrapper for Redcarpet using the PebbleMarkdown
|
|
# HTML render class.
|
|
class Markdown::PebbleMarkdownParser
|
|
def initialize(config)
|
|
@site_config = config
|
|
end
|
|
|
|
def convert(content)
|
|
content = '' if content.nil?
|
|
pbl_md = PebbleMarkdown.new(@site_config)
|
|
Redcarpet::Markdown.new(pbl_md,
|
|
fenced_code_blocks: true,
|
|
autolink: true,
|
|
tables: true,
|
|
no_intra_emphasis: true,
|
|
strikethrough: true,
|
|
highlight: true).render(content)
|
|
end
|
|
|
|
# Redcarpet HTML render class to handle the extra functionality.
|
|
class PebbleMarkdown < Redcarpet::Render::HTML
|
|
def initialize(config)
|
|
@site_config = config
|
|
super()
|
|
end
|
|
|
|
def preprocess(document)
|
|
process_links_with_double_backticks(document)
|
|
process_double_backticks(document)
|
|
document
|
|
end
|
|
|
|
# Add ID and anchors to all headers.
|
|
def header(text, header_level)
|
|
if text.include?('<')
|
|
id = Nokogiri::HTML(text).text.slugize
|
|
else
|
|
id = text.slugize
|
|
end
|
|
str = "<h#{header_level} id=\"#{id}\" class=\"anchor\">"
|
|
str += text
|
|
str += "</h#{header_level}>"
|
|
str
|
|
end
|
|
|
|
def paragraph(text)
|
|
if (match = /^\^(CP|LC)\^/.match(text))
|
|
"<p class=\"platform-specific\" data-sdk-platform=\"#{shortcode_to_platform(match[1])}\">#{text[(match[1].length + 2)..-1].strip}</p>"
|
|
else
|
|
"<p>#{text}</p>"
|
|
end
|
|
end
|
|
|
|
# Use Pygments to generate the syntax highlighting markup.
|
|
def block_code(code, language)
|
|
classes = ['highlight']
|
|
if /^nc\|/.match(language)
|
|
classes << 'no-copy'
|
|
language = language[3..-1]
|
|
end
|
|
if language == 'text'
|
|
"<div class=\"#{classes.join(' ')}\"><pre>#{code}</pre></div>"
|
|
else
|
|
set_classes(Pygments.highlight(code, lexer: language), classes)
|
|
end
|
|
end
|
|
|
|
def link(url, title, content)
|
|
if content == 'EMBED'
|
|
embed(url)
|
|
else
|
|
classes = []
|
|
if /^DOCS:/.match(title)
|
|
title = title[5..-1]
|
|
classes << 'link--docs'
|
|
end
|
|
# URL begins with a single slash (but not double slash)
|
|
url = baseurl + url if %r{^/[^/]}.match(url)
|
|
data_str = ''
|
|
if (match = regex_button.match(content))
|
|
classes << 'btn'
|
|
classes << 'btn--markdown'
|
|
classes.concat(match[3].split(',').map { |cls| 'btn--' + cls })
|
|
content = match[1]
|
|
end
|
|
if (match = regex_link_data.match(title))
|
|
match[3].split(',').each do |item|
|
|
item = item.split(':')
|
|
data_str += ' data-' + item[0] + '="' + item[1] + '"'
|
|
end
|
|
title = match[1]
|
|
end
|
|
"<a href=\"#{url}\"" \
|
|
" title=\"#{title}\"" \
|
|
" class=\"#{classes.join(' ')}\"#{data_str}>#{content}</a>"
|
|
end
|
|
end
|
|
|
|
# Better image handling.
|
|
# * Add size specificiations (taken from RDiscount)
|
|
# * Prepend the site baselink to images that beings with /
|
|
# TODO: Handle the cases where image link begins with //
|
|
# TODO: Maybe add additional style choices (centered, inline, etc)
|
|
def image(link, title, alt_text)
|
|
if (size_match = /^(.*)\ =([0-9]+)x?([0-9]*)$/.match(link))
|
|
link = size_match[1]
|
|
width = size_match[2]
|
|
height = size_match[3]
|
|
end
|
|
|
|
classes = []
|
|
if (match = regex_button.match(alt_text))
|
|
classes.concat(match[3].split(','))
|
|
alt_text = match[1]
|
|
end
|
|
|
|
link = asset_path + link if %r{^/[^/]}.match(link)
|
|
|
|
img_str = "<img src=\"#{link}\""
|
|
img_str += " title=\"#{title}\"" unless title.to_s.empty?
|
|
img_str += " alt=\"#{alt_text}\"" unless alt_text.to_s.empty?
|
|
img_str += " width=\"#{width}\"" unless width.to_s.empty?
|
|
img_str += " height=\"#{height}\"" unless height.to_s.empty?
|
|
img_str += " class=\"#{classes.join(' ')}\"" unless classes.empty?
|
|
img_str += ' />'
|
|
img_str
|
|
end
|
|
|
|
private
|
|
|
|
# This is used to process links that contain double backticks.
|
|
# For example:
|
|
# [click me](``Window``)
|
|
# This allows for the text of a link to be different than the name
|
|
# of the symbol you are linking to.
|
|
def process_links_with_double_backticks(document)
|
|
# Skip the warning, we don't want docs or links to them
|
|
if !@site_config['skip_docs'].nil? && (@site_config['skip_docs'] == 'true')
|
|
return
|
|
end
|
|
|
|
document.gsub!(/(\[([^\]]+)\])\(``([^`]+)``\)/) do |str|
|
|
url = Regexp.last_match[3]
|
|
text = Regexp.last_match[2]
|
|
text_in_brackets = Regexp.last_match[1]
|
|
|
|
language, symbol = parse_symbol(url)
|
|
entry = docs_lookup(symbol, language)
|
|
Jekyll.logger.warn('Backtick Warning:', "Could not find symbol '#{text}'") if entry.nil?
|
|
|
|
entry ? (text_in_brackets + "#{backtick_link(entry)}") : text
|
|
end
|
|
end
|
|
|
|
def process_double_backticks(document)
|
|
# Skip the warning, we don't want docs or links to them
|
|
if !@site_config['skip_docs'].nil? && (@site_config['skip_docs'] == 'true')
|
|
return
|
|
end
|
|
|
|
document.gsub!(/([^`]+|\A)``([^`]+)``/) do |str|
|
|
language, symbol = parse_symbol(Regexp.last_match[2])
|
|
entry = docs_lookup(symbol, language)
|
|
if entry.nil?
|
|
Jekyll.logger.warn('Backtick Warning:', "Could not find symbol '#{Regexp.last_match[2]}'")
|
|
language.nil? ? str : ('``' + Regexp.last_match[2][language.size+1..-1] + '``')
|
|
else
|
|
symbol_str = Regexp.last_match[2]
|
|
symbol_str = symbol_str[language.size+1..-1] unless language.nil?
|
|
"#{Regexp.last_match[1]}[`#{symbol_str}`]#{backtick_link(entry)}"
|
|
end
|
|
end
|
|
end
|
|
|
|
def backtick_link(symbol)
|
|
"(#{symbol[:url]} \"DOCS:#{symbol[:name]}\")"
|
|
end
|
|
|
|
# Split the documentation string into language and symbol name.
|
|
def parse_symbol(str)
|
|
match = /^([a-z]*:)?([A-Za-z0-9_:\.\ ]*)/.match(str)
|
|
language = match[1]
|
|
language = language[0..-2].downcase unless language.nil?
|
|
name = match[2]
|
|
return language, name
|
|
end
|
|
|
|
def docs_lookup(name, language)
|
|
return nil if name.nil?
|
|
@site_config[:docs][:symbols].find do |symbol|
|
|
symbol[:name].downcase == name.downcase &&
|
|
(language.nil? ? true : symbol[:language] == language)
|
|
end
|
|
end
|
|
|
|
def embed(url)
|
|
if (match = regex_youtube_video.match(url))
|
|
youtube(match[2])
|
|
elsif (match = regex_youtube_playlist.match(url))
|
|
youtube_playlist(match[3])
|
|
elsif (match = regex_vimeo_video.match(url))
|
|
vimeo(match[1])
|
|
elsif (match = regex_slideshare.match(url))
|
|
slideshare(match[1])
|
|
elsif (match = regex_gist.match(url))
|
|
gist(match[2])
|
|
end
|
|
end
|
|
|
|
def set_classes(html, classes)
|
|
doc = Nokogiri::HTML::DocumentFragment.parse(html)
|
|
doc.child['class'] = classes.join(' ')
|
|
doc.to_html
|
|
end
|
|
|
|
def youtube(id)
|
|
'<div class="embed embed--youtube"><div class="video-wrapper">' \
|
|
'<iframe width="640" height="360" frameborder="0" allowfullscreen' \
|
|
" src=\"//www.youtube.com/embed/#{id}?rel=0\" ></iframe>" \
|
|
'</div></div>'
|
|
end
|
|
|
|
def youtube_playlist(id)
|
|
'<div class="embed embed--youtube"><div class="video-wrapper">' \
|
|
'<iframe frameborder="0" allowfullscreen'\
|
|
" src=\"//www.youtube.com/embed/videoseries?list=#{id}\" ></iframe>" \
|
|
'</div></div>'
|
|
end
|
|
|
|
def vimeo(id)
|
|
'<div class="embed embed--vimeo"><div class="video-wrapper">' \
|
|
'<iframe width="500" height="281" frameborder="0"' \
|
|
' webkitallowfullscreen mozallowfullscreen allowfullscreen' \
|
|
" src=\"//player.vimeo.com/video/#{id}\"></iframe>" \
|
|
'</div></div>'
|
|
end
|
|
|
|
def slideshare(id)
|
|
'<div style="width: 100%; height: 0px; position: relative; padding-bottom: 63%;">' \
|
|
"<iframe src=\"https://www.slideshare.net/slideshow/embed_code/key/#{id}\"" \
|
|
' frameborder="0" allowfullscreen style="width: 100%; height: 100%; position: absolute;">' \
|
|
'</iframe>'\
|
|
'</div>'
|
|
end
|
|
|
|
def gist(id)
|
|
'<div class="embed embed--gist">' \
|
|
"<script src=\"//gist.github.com/#{id}.js\"></script>" \
|
|
'</div>'
|
|
end
|
|
|
|
def baseurl
|
|
@site_config['baseurl'] || ''
|
|
end
|
|
|
|
def asset_path
|
|
@site_config['asset_path'] || ''
|
|
end
|
|
|
|
def link_sdk(url, title, content)
|
|
|
|
end
|
|
|
|
def regex_youtube_video
|
|
%r{youtube\.com/(watch\?v=|v/|embed/)([a-z0-9A-Z\-_]*)}
|
|
end
|
|
|
|
def regex_youtube_playlist
|
|
%r{^(https?://)?([w]{3}\.)?youtube\.com/playlist\?list=([a-z0-9A-Z\-]*)}
|
|
end
|
|
|
|
def regex_vimeo_video
|
|
%r{vimeo.com/video/([0-9]+)}
|
|
end
|
|
|
|
def regex_slideshare
|
|
%r{slideshare.net/slideshow/embed_code/key/([a-z0-9A-Z]*)}
|
|
end
|
|
|
|
def regex_gist
|
|
%r{^(https?://)?gist.github\.com/(.*)}
|
|
end
|
|
|
|
def regex_button
|
|
/^(.*)\ (>|>)\{?([a-z,0-9\-]*)\}?$/
|
|
end
|
|
|
|
def regex_link_data
|
|
/^(.*)\ (>|>)\{([a-z\-_,:0-9A-Z]+)\}$/
|
|
end
|
|
|
|
def shortcode_to_platform(shortcode)
|
|
platforms = {
|
|
'CP' => 'cloudpebble',
|
|
'LC' => 'local'
|
|
}
|
|
platforms[shortcode]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|