Import the pebble dev site into devsite/

This commit is contained in:
Katharine Berry 2025-02-17 17:02:33 -08:00
parent 3b92768480
commit 527858cf4c
1359 changed files with 265431 additions and 0 deletions

View file

@ -0,0 +1,53 @@
# 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.
# Generates a list page for each blog author.
# The list of authors is in /source/_data/blog_authors.yml
module Jekyll
class AuthorPage < Page
def initialize(site, base, dir, author)
@site = site
@base = base
@dir = dir
@name = author[0] + '/index.html'
self.process(@name)
self.read_yaml(File.join(base, '_layouts'), 'blog/author_page.html')
self.data['posts'] = site.posts.docs.select { |post| post['author'] == author[0] }
self.data['author_name'] = author[1]['name']
self.data['author'] = author[0]
self.data['title'] = "Pebble Developer Blog: #{author[1]['name']}"
end
end
class AuthorPageGenerator < Generator
def generate(site)
if ! site.layouts.key? 'blog/author_page'
throw 'Layout for the blog author pages is missing.'
end
dir = site.config['tag_dir'] || 'blog/authors'
site.data['authors'].each do |author|
author[1]['num_posts'] = site.posts.docs.select { |post| post['author'] == author[0] }.length
site.pages << AuthorPage.new(site, site.source, dir, author)
end
end
end
end

View file

@ -0,0 +1,69 @@
# 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.
# Generates a list page for each blog tag.
require 'slugize'
module Jekyll
class TagPage < Page
def initialize(site, base, dir, tag)
@site = site
@base = base
@dir = dir
@name = tag[0].slugize + '/index.html'
self.process(@name)
self.read_yaml(File.join(base, '_layouts'), 'blog/tag_page.html')
self.data['posts'] = tag[1].sort_by(&:date).reverse
self.data['name'] = tag[0]
self.data['tag'] = tag[0]
self.data['title'] = "Pebble Developer Blog: #{tag[0]}"
end
end
class TagPageRedirect < Page
def initialize(site, base, dir, tag)
@site = site
@base = base
@dir = dir
@name = tag[0].slugize + '.html'
self.process(@name)
self.read_yaml(File.join(base, '_layouts'), 'utils/redirect_permanent.html')
self.data['path'] = '/' + File.join(dir, tag[0].slugize) + '/'
end
end
class TagPageGenerator < Generator
def generate(site)
if ! site.layouts.key? 'blog/tag_page'
throw 'Layout for the blog tag pages is missing.'
end
dir = site.config['tag_dir'] || 'blog/tags'
site.tags.each do |tag|
site.pages << TagPage.new(site, site.source, dir, tag)
site.pages << TagPageRedirect.new(site, site.source, dir, tag)
end
end
end
end

View file

@ -0,0 +1,50 @@
# 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.
module Jekyll
class EnvironmentGenerator < Generator
priority :highest
def initialize(site)
# TODO: Figure out how to check for the environment type.
require 'dotenv'
Dotenv.load
end
def generate(site)
if !ENV.has_key?('URL') && ENV.has_key?('HEROKU_APP_NAME')
ENV['URL'] = "https://#{ENV['HEROKU_APP_NAME']}.herokuapp.com"
ENV['HTTPS_URL'] = "https://#{ENV['HEROKU_APP_NAME']}.herokuapp.com"
end
site.data['env'].each do |item|
if ENV.has_key?(item['env'])
set_config(site.config, item['config'], ENV[item['env']])
elsif item.has_key?('default')
set_config(site.config, item['config'], item['default'])
end
end
end
private
# TODO: Rewrite this function to allow for nested keys.
def set_config(config, key, value)
config[key] = value
end
end
end

View file

@ -0,0 +1,23 @@
# 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.
# FilterAssetify is a Liquid filter to prepend the asset_path when needed
module FilterAssetify
def assetify(input)
asset_path = @context.registers[:site].config['asset_path']
%r{^/[^/]}.match(input) ? (asset_path + input) : input
end
end
Liquid::Template.register_filter(FilterAssetify)

View file

@ -0,0 +1,21 @@
# 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.
module FilterBasename
def basename(input, suffix)
File.basename(input, suffix)
end
end
Liquid::Template.register_filter(FilterBasename)

View file

@ -0,0 +1,28 @@
# 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.
module FilterFakePlatform
def fake_platform(input)
case input
when 'aplite'
'SDK 3'
when 'basalt'
'SDK 4'
else
'??'
end
end
end
Liquid::Template.register_filter(FilterFakePlatform)

View file

@ -0,0 +1,29 @@
# 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.
module FilterHashSort
def hash_sort(hash, property=nil)
return [] if hash.nil?
sorted_hash = []
hash.each { |key, value| sorted_hash << [key, value] }
if property.nil?
sorted_hash.sort! { |a, b| a[0] <=> b[0] }
else
sorted_hash.sort! { |a, b| a[1][property] <=> b[1][property] }
end
sorted_hash
end
end
Liquid::Template.register_filter(FilterHashSort)

View file

@ -0,0 +1,28 @@
# 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.
# Liquid filter that does magic to make plurals easy.
module Pluralize
def pluralize(number, singular, plural = nil)
if number == 1
"#{number} #{singular}"
elsif plural.nil?
"#{number} #{singular}s"
else
"#{number} #{plural}"
end
end
end
Liquid::Template.register_filter(Pluralize)

View file

@ -0,0 +1,25 @@
# 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 'slugize'
# Liquid filter that turns a string into a slug.
# Used to turn tag names into the tag URL part.
module FilterSlugize
def slugize(input)
input.slugize
end
end
Liquid::Template.register_filter(FilterSlugize)

View file

@ -0,0 +1,236 @@
# 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 'htmlentities'
require 'algoliasearch'
require 'slugize'
require 'dotenv'
require 'securerandom'
module Jekyll
class GeneratorAlgolia < Generator
# Do this last so everything else has been processed already.
priority :lowest
def initialize(_config)
Dotenv.load
end
def generate(site)
@site = site
return unless check_config?
@prefix = site.config['algolia_prefix'] || ''
@random_code = random_code
Algolia.init(application_id: site.config['algolia_app_id'],
api_key: site.config['algolia_api_key'])
@indexes = setup_indexes
generate_all
end
private
def check_config?
if @site.config['algolia_app_id'].nil? || @site.config['algolia_app_id'].empty?
Jekyll.logger.warn(
'Config Warning:',
'You did not provide a ALGOLIA_APP_ID environment variable.'
)
return false
end
if @site.config['algolia_api_key'].nil? || @site.config['algolia_api_key'].empty?
Jekyll.logger.warn(
'Config Warning:',
'You did not provide a ALGOLIA_API_KEY environment variable.'
)
return false
end
true
end
def generate_all
generate_blog_posts
generate_guides
generate_documentation
generate_none_guide_guides
generate_other
end
def random_code
SecureRandom.hex
end
def setup_indexes
indexes = {}
@site.data['search_indexes'].each do |name, properties|
index = Algolia::Index.new(@prefix + name)
unless properties['settings'].nil?
index.set_settings(properties['settings'])
end
indexes[name] = index
end
indexes
end
def generate_documentation
return if @site.data['docs'].nil?
documents = @site.data['docs'][:symbols].map do |item|
next if item[:language] == 'c_preview'
if item[:summary].nil? || item[:summary].strip.length == 0
Jekyll.logger.warn(
'Search Warning:',
"There was no summary for the symbol '#{item[:name]}' in #{item[:language]}."
)
end
{
'objectID' => item[:url],
'title' => item[:name],
'splitTitle' => item[:name].split(/(?=[A-Z])/).join(' '),
'url' => item[:url],
'summary' => item[:summary],
'kind' => item[:kind],
'language' => item[:language],
'type' => 'documentation',
'ranking' => doc_language_rank[item[:language]] * 1000,
'randomCode' => @random_code
}
end.compact
@indexes['documentation'].save_objects(documents)
end
def generate_blog_posts
documents = []
@site.posts.docs.each do | post |
# Calculate the age of the post so we can prioritise newer posts
# over older ones.
# NOTE: post.date is actually a Time object, despite its name
age = (Time.now - post.date).round
author = post.data['author']
post.get_sections.each do | section |
# Ignore sections without any contents.
if section[:contents].strip.size == 0
next
end
if section[:title].nil?
url = post.url
else
url = post.url + '#' + section[:title].slugize
end
document = {
'objectID' => url,
'title' => post.data['title'],
'sectionTitle' => section[:title],
'url' => url,
'urlDisplay' => post.url,
'author' => author,
'content' => HTMLEntities.new.decode(section[:contents]),
'posted' => post.date,
'age' => age,
'type' => 'blog post',
'randomCode' => @random_code
}
documents << document
end
end
@indexes['blog-posts'].save_objects(documents)
end
def generate_guides
documents = []
return if @site.collections['guides'].nil?
@site.collections['guides'].docs.each do | guide |
group = @site.data['guides'][guide.data['guide_group']]
unless group.nil? || group['subgroups'].nil? || guide.data['guide_subgroup'].nil?
subgroup = group.nil? ? '' : group['subgroups'][guide.data['guide_subgroup']]
end
guide.get_sections.each do | section |
url = guide.url
unless section[:title].nil?
url = url + '#' + section[:title].slugize
end
document = {
'objectID' => url,
'title' => guide.data['title'],
'sectionTitle' => section[:title],
'url' => url,
'urlDisplay' => guide.url,
'content' => HTMLEntities.new.decode(section[:contents]),
'group' => group.nil? ? '' : group['title'],
'subgroup' => subgroup.nil? ? '' : subgroup['title'],
'type' => 'guide',
'randomCode' => @random_code
}
documents << document
end
end
@indexes['guides'].save_objects(documents)
end
def generate_none_guide_guides
documents = []
gs_pages = @site.pages.select { |page| page.data['search_index'] }
gs_pages.each do |page|
page.get_sections.each do |section|
url = page.url
url = url + '#' + section[:title].slugize unless section[:title].nil?
document = {
'objectID' => url,
'title' => page.data['title'],
'sectionTitle' => section[:title],
'url' => url,
'urlDisplay' => page.url,
'content' => HTMLEntities.new.decode(section[:contents]),
'group' => page.data['search_group'],
'subgroup' => page.data['sub_group'],
'type' => 'not-guide',
'randomCode' => @random_code
}
documents << document
end
end
@indexes['guides'].save_objects(documents)
end
def generate_other
documents = @site.data['search-other'].map do |other|
{
'objectID' => other['id'],
'title' => other['title'],
'url' => other['url'],
'content' => other['description'],
'randomCode' => @random_code
}
end
@indexes['other'].save_objects(documents)
end
def doc_language_rank
{
'c' => 10,
'rockyjs' => 9,
'pebblekit_js' => 8,
'pebblekit_android' => 6,
'pebblekit_ios' => 4
}
end
end
end

View file

@ -0,0 +1,166 @@
# 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 'open-uri'
require 'zip'
require 'nokogiri'
require_relative '../lib/pebble_documentation_pebblekit_android.rb'
require_relative '../lib/pebble_documentation_c.rb'
require_relative '../lib/pebble_documentation_js.rb'
require_relative '../lib/pebble_documentation_pebblekit_ios.rb'
require_relative '../lib/toc_generator.rb'
OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
OpenURI::Buffer.const_set 'StringMax', 0
# Master plugins for processing the documentation on the site.
# The actual work is done in individual classes for each language type.
# Each class generates three types of data:
# - Symbols
# - Pages
# - Tree
#
# The Symbols are a list of objects that are things such as methods, classes
# or enums, that are used to populate the search indexes, and power the double
# backtick docs linking.
#
# The Pages are the Jekyll pages that will be part of the built site and are
# what the user will see.
#
# The Tree is used to build the site navigation.
#
# Note: The docs_url variable is created from the environment.
# See environment.md for more information.
module Jekyll
class DocsGenerator < Generator
priority :high
def generate(site)
@site = site
@docs = {
symbols: [],
pages: [],
tree: {}
}
if @site.config['docs_url'].nil? || @site.config['docs_url'].empty?
Jekyll.logger.warn(
'Config Warning:',
'You did not provide a DOCS_URL environment variable.'
)
elsif !@site.config['skip_docs'].nil? && (@site.config['skip_docs'] == 'true')
Jekyll.logger.info('Docs Generation:', 'Skipping documentation generation...')
else
Jekyll.logger.info('Docs Generation:', 'Generating pages...')
generate_docs
render_pages
Jekyll.logger.info('Docs Generation:', 'Done.')
end
set_data
end
private
def generate_docs
# The order of these functions will determine the order of preference
# when looking up symbols e.g. double backticks
# DO NOT CHANGE THE ORDER UNLESS YOU KNOW WHAT YOU ARE DOING
generate_docs_c
generate_docs_c_preview unless @site.data['docs']['c_preview'].nil?
generate_docs_rocky_js
generate_docs_pebblekit_js
generate_docs_pebblekit_android
generate_docs_pebblekit_ios
end
def render_pages
@docs[:pages].each { |page| @site.pages << page }
end
def set_data
# A somewhat ugly hack to let the Markdown parser have access
# to this data.
@site.config[:docs] = @docs
@site.data['docs'] = @docs
# Another ugly hack to make accessing the data much easier from Liquid.
@site.data['docs_tree'] = JSON.parse(JSON.dump(@docs[:tree]))
@site.data['symbols'] = JSON.parse(JSON.dump(@docs[:symbols]))
end
def generate_docs_c
docs = Pebble::DocumentationC.new(
@site,
@site.config['docs_url'] + @site.data['docs']['c'],
'/docs/c/'
)
load_data(docs, :c)
end
def generate_docs_c_preview
docs = Pebble::DocumentationC.new(
@site,
@site.config['docs_url'] + @site.data['docs']['c_preview'],
'/docs/c/preview/',
'c_preview'
)
load_data(docs, :c_preview)
end
def generate_docs_rocky_js
docs = Pebble::DocumentationJs.new(
@site,
@site.data['docs']['rocky_js'],
'/docs/rockyjs/',
'rockyjs',
true
)
load_data(docs, :rockyjs)
end
def generate_docs_pebblekit_js
docs = Pebble::DocumentationJs.new(
@site,
@site.data['docs']['pebblekit_js'],
'/docs/pebblekit-js/',
'pebblekit_js'
)
load_data(docs, :pebblekit_js)
end
def generate_docs_pebblekit_android
docs = Pebble::DocumentationPebbleKitAndroid.new(
@site,
@site.config['docs_url'] + @site.data['docs']['pebblekit_android']
)
load_data(docs, :pebblekit_android)
end
def generate_docs_pebblekit_ios
docs = Pebble::DocumentationPebbleKitIos.new(
@site,
@site.config['docs_url'] + @site.data['docs']['pebblekit_ios'],
'/docs/pebblekit-ios/'
)
load_data(docs, :pebblekit_ios)
end
def load_data(docs, type)
@docs[:tree][type] = []
docs.load_symbols(@docs[:symbols])
docs.create_pages(@docs[:pages])
docs.build_tree(@docs[:tree][type])
end
end
end

View file

@ -0,0 +1,65 @@
# 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.
module Jekyll
class GeneratorExamples < Generator
def initialize(_config)
end
def generate(site)
@site = site
process_tags
process_languages
process_hardware_platforms
@site.data['examples_metadata'] = {
'tags' => @tags,
'languages' => @languages,
'hardware_platforms' => @hardware_platforms
}
end
def process_tags
@tags = {}
@site.data['examples'].each do |example|
next if example['tags'].nil?
example['tags'].each do |tag|
@tags[tag] = { 'count' => 0 } unless @tags.has_key?(tag)
@tags[tag]['count'] += 1
end
end
end
def process_languages
@languages = {}
@site.data['examples'].each do |example|
next if example['languages'].nil?
example['languages'].each do |language|
@languages[language] = { 'count' => 0 } unless @languages.has_key?(language)
@languages[language]['count'] += 1
end
end
end
def process_hardware_platforms
@hardware_platforms = {}
@site.data['examples'].each do |example|
next if example['hardware_platforms'].nil?
example['hardware_platforms'].each do |hw|
@hardware_platforms[hw] = { 'count' => 0 } unless @hardware_platforms.has_key?(hw)
@hardware_platforms[hw]['count'] += 1
end
end
end
end
end

View file

@ -0,0 +1,94 @@
# 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 'htmlentities'
require 'algoliasearch'
require 'slugize'
require 'dotenv'
module Jekyll
class GeneratorGuides < Generator
priority :highest
def initialize(config)
end
def generate(site)
@site = site
site.collections['guides'].docs.each do |guide|
group = find_group(guide)
subgroup = find_subgroup(guide, group)
guide.data['group_data'] = group
guide.data['subgroup_data'] = subgroup
unless subgroup.nil?
subgroup['guides'] << {
'title' => guide.data['title'],
'url' => guide.url,
'menu' => guide.data['menu'],
'order' => guide.data['order'],
'summary' => guide.data['description']
}
else
unless group.nil?
group['guides'] << {
'title' => guide.data['title'],
'url' => guide.url,
'menu' => guide.data['menu'],
'order' => guide.data['order'],
'summary' => guide.data['description']
}
end
end
end
site.data['guides'] = [] if site.data['guides'].nil?
site.data['guides'].each do |id, group|
group['url'] = "/guides/#{id}/"
if group['subgroups'].nil?
group['subgroups'] = []
next
end
group['subgroups'].each do |id, subgroup|
subgroup['url'] = "#{group['url']}#{id}/"
end
end
end
def find_group(guide)
@site.data['guides'].each do |id, group|
if id == guide.data['guide_group']
group['guides'] = [] if group['guides'].nil?
return group
end
end
nil
end
def find_subgroup(guide, group)
return if group.nil? || group['subgroups'].nil?
group['subgroups'].each do |id, subgroup|
if id == guide.data['guide_subgroup']
subgroup['guides'] = [] if subgroup['guides'].nil?
return subgroup
end
end
nil
end
end
end

View file

@ -0,0 +1,35 @@
# 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 'googlestaticmap'
module Jekyll
class GeneratorMeetups < Generator
def initialize(config)
end
def generate(site)
@site = site
map = GoogleStaticMap.new(:width => 700, :height => 500)
site.data['meetups'].each do |meetup|
map.markers << MapMarker.new(:color => "0x9D49D5FF",
:location => MapLocation.new(:latitude => meetup['pin']['latitude'],
:longitude => meetup['pin']['longitude']
)
)
end
@site.data['meetups_map_url'] = map.url(:auto)
end
end
end

View file

@ -0,0 +1,58 @@
# 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 'uglifier'
require 'digest'
module Jekyll
# Jekyll Generator for concatenating and minifying JS for production site
class GeneratorMinifyJS < Generator
priority :highest
def initialize(_)
end
def generate(site)
return if site.config['rack_env'] == 'development'
@site = site
@tmp_dir = File.join(site.source, '../tmp/')
@js_dir = 'assets/js/'
@tmp_js_dir = File.join(@tmp_dir, @js_dir)
libs_js = uglify_libs
libs_md5 = Digest::MD5.hexdigest(libs_js)
@site.data['js']['lib_hash'] = libs_md5
create_libs_js(libs_js, libs_md5)
end
private
def uglify_libs
ugly_libs = []
@site.data['js']['libs'].each do |lib|
lib_path = File.join(@site.source, 'assets', lib['path'])
ugly_libs << Uglifier.compile(File.read(lib_path))
end
ugly_libs.join("\n\n")
end
def create_libs_js(js, md5)
FileUtils.mkdir_p(@tmp_js_dir)
File.open(File.join(@tmp_js_dir, "libs-#{md5}.js"), 'w') do |f|
f.write(js)
end
@site.static_files << Jekyll::StaticFile.new(@site, @tmp_dir, @js_dir,
"libs-#{md5}.js")
end
end
end

View file

@ -0,0 +1,62 @@
# 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.
module Jekyll
class GeneratorRedirects < Generator
priority :lowest
def initialize(config)
end
def generate(site)
@site = site
site.data['redirects'].each do |redirect|
if is_infinite_redirect?(redirect[0], redirect[1])
Jekyll.logger.warn "Redirect Warning:", "Skipping redirect of #{redirect[0]} to #{redirect[1]}"
next
end
@site.pages << RedirectPage.new(@site, @site.source, File.dirname(redirect[0]), File.basename(redirect[0]), redirect[1])
end
end
private
# Returns true if the redirect pair (from, to) will cause an infinite
# redirect.
def is_infinite_redirect?(from, to)
return true if from == to
return true if File.basename(from) == 'index.html' && File.dirname(from) == File.dirname(to + 'index.html')
false
end
end
class RedirectPage < Page
def initialize(site, base, dir, name, redirect_to)
@site = site
@base = base
@dir = dir
@name = name.empty? ? 'index.html' : name
self.process(@name)
self.read_yaml(File.join(base, '_layouts', 'utils'), 'redirect_permanent.html')
self.data['redirect_to'] = redirect_to
end
end
end

View file

@ -0,0 +1,54 @@
# 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_relative '../lib/search_markdown'
module Jekyll
module Convertible
def get_output
process_search
@search_markdown.get_contents
end
def get_sections
process_search
@search_markdown.get_sections
end
private
def process_search
unless @search_markdown.nil?
return
end
@search_markdown = Pebble::SearchMarkdown.new()
redcarpet = Redcarpet::Markdown.new(@search_markdown,
fenced_code_blocks: true,
autolink: true,
tables: true,
no_intra_emphasis: true,
strikethrough: true,
highlight: true)
payload = {}
info = { :filters => [Jekyll::Filters], :registers => { :site => site, :page => payload['page'] } }
raw_content = render_liquid(content, payload, info, '.')
redcarpet.render(raw_content)
end
end
end

View file

@ -0,0 +1,85 @@
# 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_relative '../lib/toc_generator'
module Jekyll
class Document
include Convertible
alias_method :parent_to_liquid, :to_liquid
def to_liquid
Utils.deep_merge_hashes parent_to_liquid, {
'toc' => toc,
'related_docs' => related_docs
}
end
private
def toc
unless @toc
generate_toc if data['generate_toc']
end
(@toc.nil? || @toc.empty?) ? nil : @toc
end
def related_docs
# 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
return nil if data['related_docs'].nil?
docs = data['related_docs'].map do | doc |
# Use existing doc data if it exists
if !doc.nil? and doc.is_a?(Hash) and doc.has_key?("name") and doc.has_key?("url")
doc
else
# use nil if data is formated in an unexpected way
if doc.nil? or !doc.is_a? String
next
else
# Otherwise search for the symbol
symbol = @site.config[:docs][:symbols].find do |symbol|
symbol[:name].downcase == doc.downcase
end
if symbol.nil?
Jekyll.logger.warn "Related Warning:", "Could not find symbol '#{doc}' in '#{data['title']}'"
next
else
{
'name' => symbol[:name],
'url' => symbol[:url],
}
end
end
end
end
end
def generate_toc
generator = Pebble::TocGenerator.new(data['toc_max_depth'] || -1)
@toc = generator.generate(content)
end
end
end

View file

@ -0,0 +1,45 @@
# 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_relative '../lib/toc_generator'
# Overriding the Jekyll Page class to do magic
module Jekyll
class Page
def to_liquid(attrs = ATTRIBUTES_FOR_LIQUID)
super(attrs + %w[
toc
])
end
private
def toc
unless @toc
generate_toc if data['generate_toc']
end
(@toc.nil? || @toc.empty?) ? nil : @toc
end
def generate_toc
generator = Pebble::TocGenerator.new(data['toc_max_depth'] || -1)
@toc = generator.generate(content)
end
end
end

View file

@ -0,0 +1,45 @@
# 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_relative '../lib/toc_generator'
# Overriding the Jekyll Page class to do magic
module Jekyll
class Post
def to_liquid(attrs = ATTRIBUTES_FOR_LIQUID)
super(attrs + %w[
toc
])
end
private
def toc
unless @toc
generate_toc
end
(@toc.nil? || @toc.empty?) ? nil : @toc
end
def generate_toc
generator = Pebble::TocGenerator.new(data['toc_max_depth'] || -1)
@toc = generator.generate(content)
end
end
end

View file

@ -0,0 +1,323 @@
# 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
/^(.*)\ (&gt;|>)\{?([a-z,0-9\-]*)\}?$/
end
def regex_link_data
/^(.*)\ (&gt;|>)\{([a-z\-_,:0-9A-Z]+)\}$/
end
def shortcode_to_platform(shortcode)
platforms = {
'CP' => 'cloudpebble',
'LC' => 'local'
}
platforms[shortcode]
end
end
end
end
end

View file

@ -0,0 +1,42 @@
# 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.
module Jekyll
class AlertBlock < Liquid::Block
alias_method :render_block, :render
def initialize(tag_name, text, tokens)
super
@type = text.strip
end
def render(context)
site = context.registers[:site]
converter = site.find_converter_instance(::Jekyll::Converters::Markdown)
content = converter.convert(render_block(context))
if @type == "important"
return "<div class=\"alert alert--fg-white alert--bg-dark-red\">" << "<strong>Important</strong><br/>" << "#{content}" << "</div>"
end
if @type == "notice"
return "<div class=\"alert alert--fg-white alert--bg-purple\">" << "<strong>Notice</strong><br/>" << "#{content}" << "</div>"
end
Jekyll.logger.error "Liquid Error:", "Alert type '#{@type}' is not valid. Use 'important' or 'notice'."
return ''
end
end
end
Liquid::Template.register_tag('alert', Jekyll::AlertBlock)

View file

@ -0,0 +1,31 @@
# 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.
# Liquid tag for including a style tag.
class TagAssetCss < Liquid::Tag
def initialize(tag_name, text, tokens)
super
@text = text
end
def render(context)
style = context[@text]
site = context.registers[:site]
unless %r{^//}.match(style)
style = "#{site.config['asset_path']}/css/#{style}.css"
end
"<link rel=\"stylesheet\" type=\"text/css\" href=\"#{style}\">"
end
end
Liquid::Template.register_tag('asset_css', TagAssetCss)

View file

@ -0,0 +1,31 @@
# 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.
# Liquid tag for including a script tag.
class TagAssetJs < Liquid::Tag
def initialize(tag_name, text, tokens)
super
@text = text
end
def render(context)
script = context[@text]
site = context.registers[:site]
unless %r{^//}.match(script)
script = "#{site.config['asset_path']}/js/#{script}.js"
end
"<script type=\"text/javascript\" src=\"#{script}\"></script>"
end
end
Liquid::Template.register_tag('asset_js', TagAssetJs)

View file

@ -0,0 +1,35 @@
# 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.
# Creates a link to a blog post author page and uses their full name when known.
class TagAuthorLink < Liquid::Tag
def initialize(tag_name, text, tokens)
super
@text = text
end
def render(context)
author_name = context[@text]
site = context.registers[:site]
author = site.data['authors'][author_name]
if author
url = "#{site.baseurl}/blog/authors/#{author_name}/"
"<a href=\"#{url}\">#{author['name']}</a>"
else
author_name
end
end
end
Liquid::Template.register_tag('author_link', TagAuthorLink)

View file

@ -0,0 +1,36 @@
# 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.
class TagAuthorPhoto < Liquid::Tag
def initialize(tag_name, text, tokens)
super
pieces = text.split(' ')
@name = pieces[0]
@size = pieces[1].to_i
end
def render(context)
author_name = context[@name]
site = context.registers[:site]
author = site.data['authors'][author_name]
unless author && author['photo']
author = site.data['authors']['pebble']
end
photo = author['photo']
photo = site.config['asset_path'] + photo if %r{^/[^/]}.match(photo)
"<img src=\"#{photo}\" width=#{@size} height=#{@size}>"
end
end
Liquid::Template.register_tag('author_photo', TagAuthorPhoto)

View file

@ -0,0 +1,58 @@
# 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.
# Liquid inline tag to produce an Edit Gist in CloudPebble button
class TagCloudPebbleEditGist < Liquid::Tag
def initialize(tag_name, gist_id, tokens)
super
@gist_id = gist_id.strip
end
def render(context)
page = context.registers[:page]
extension = File.extname(page['name'])
if extension == '.md'
render_markdown
else
render_html
end
end
def render_markdown
"[#{content} >{#{markdown_classes}}](#{url})"
end
def render_html
"<a href=\"#{url}\" title=\"\" class=\"#{html_classes}\">#{content}</a>"
end
private
def url
"https://cloudpebble.net/ide/gist/#{@gist_id}"
end
def html_classes
'btn btn--wide btn--pink'
end
def markdown_classes
'wide,pink'
end
def content
'Edit in CloudPebble'
end
end
Liquid::Template.register_tag('cloudpebble_edit_gist', TagCloudPebbleEditGist)

View file

@ -0,0 +1,43 @@
# 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.
# Liquid tag that displays the names of everyone who contributed on the file.
class TagGitContributors < Liquid::Tag
def initialize(tag_name, text, tokens)
super
@text = text
end
def render(context)
list = '<ul class="git-contributors">'
contributors(context).each do |name|
list += "<li>#{name}</li>" unless name.empty?
end
list += '</ul>'
list
end
private
def contributors(context)
site = context.registers[:site]
page = context[@text]
file_path = page['relative_path'] || page['path']
full_path = './' + site.config['source'] + file_path
names = `git log --follow --format='%aN |' "#{full_path}" | sort -u`
names.split('|').map { |name| name.strip }
end
end
Liquid::Template.register_tag('git_contributors', TagGitContributors)

View file

@ -0,0 +1,67 @@
# 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.
class TagGuideLink < Liquid::Tag
def initialize(tag_name, text, tokens)
super
@text = text.strip
end
def make_html(title, url)
return "<a href=\"#{url}\"><em>#{title}</em></a>"
end
def render(context)
guide_path = @text.split('#')[0].strip
guide_hash = (@text.split('#').length > 1 ? @text.split('#')[1] : '').strip
if guide_hash.length > 1
guide_hash = (guide_hash.split(' ')[0]).strip
end
# Custom title?
guide_title = nil
index = @text.index('"')
if index != nil
guide_title = (@text.split('"')[1]).strip
guide_title = guide_title.gsub('"', '')
guide_path = guide_path.split(' ')[0]
end
site = context.registers[:site]
site.collections['guides'].docs.each do |guide|
path = guide.relative_path.sub(/^_guides\//, '').sub(/\.md$/, '')
# Check if it's a 'section/guide' path
if path.index('/') != nil
if path == guide_path
return make_html(guide_title != nil ? guide_title : guide.data['title'],
"#{guide.url}#{guide_hash == '' ? '' : "##{guide_hash}"}")
end
end
# Check if it's a 'section' path
site.data['guides'].each do |id, group|
if id == guide_path
return make_html(guide_title != nil ? guide_title : group['title'], "/guides/#{guide_path}")
end
end
end
# No match
Jekyll.logger.error "Liquid Error:", "Could not find the guide or section for #{@text}."
return ''
end
end
Liquid::Template.register_tag('guide_link', TagGuideLink)

View file

@ -0,0 +1,35 @@
# 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 'pygments'
# Liquid block tag to run code through Pygments syntax highlighter.
class Highlight < Liquid::Block
def initialize(tag_name, markup, tokens)
super
options = JSON.parse(markup)
return unless options
@language = options['language']
@classes = options['classes']
@options = options['options'] || {}
end
def render(context)
str = Pygments.highlight(super.strip, lexer: @language, options: @options)
str.gsub!(/<div class="highlight"><pre>/,
"<div class=\"highlight #{@classes}\"><pre>")
str
end
end
Liquid::Template.register_tag('highlight', Highlight)

View file

@ -0,0 +1,38 @@
# 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.
class TagIfStartsWith < Liquid::Block
def initialize(tag_name, text, tokens)
super
matches = /^([A-Za-z\.\_\-\/]+) ([A-Za-z\.\_\-\/\']+)$/.match(text.strip)
@pieces = {
:outer => matches[1],
:inner => matches[2]
}
@text = text
end
def render(context)
outer = context[@pieces[:outer]]
inner = context[@pieces[:inner]]
if inner.nil? || outer.nil?
return ""
end
if outer.downcase.start_with?(inner.downcase)
return super.to_s.strip
end
""
end
end
Liquid::Template.register_tag('if_starts_with', TagIfStartsWith)

View file

@ -0,0 +1,30 @@
# 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.
class TagImportJs < Liquid::Tag
def initialize(tag_name, text, tokens)
super
@text = text
end
def render(context)
site = context.registers[:site]
filename = File.join(site.source, "_js", @text).strip
File.read(filename)
end
end
Liquid::Template.register_tag('import_js', TagImportJs)

View file

@ -0,0 +1,35 @@
# 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.
module Jekyll
class MarkdownBlock < Liquid::Block
alias_method :render_block, :render
def initialize(tag_name, markup, tokens)
super
end
# Uses the default Jekyll markdown parser to
# parse the contents of this block
#
def render(context)
site = context.registers[:site]
converter = site.find_converter_instance(::Jekyll::Converters::Markdown)
converter.convert(render_block(context))
end
end
end
Liquid::Template.register_tag('markdown', Jekyll::MarkdownBlock)

View file

@ -0,0 +1,39 @@
# 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.
module Jekyll
class PlatformBlock < Liquid::Block
alias_method :render_block, :render
def initialize(tag_name, text, tokens)
super
@platform = text.strip
end
def render(context)
if (@platform == "local") || (@platform == "cloudpebble")
site = context.registers[:site]
converter = site.find_converter_instance(::Jekyll::Converters::Markdown)
content = converter.convert(super)
return "<div class=\"platform-specific\" data-sdk-platform=\"#{@platform}\">#{content}</div>"
end
Jekyll.logger.error "Liquid Error:", "Platform '#{@platform}' is not valid. Use 'local' or 'cloudpebble'."
return ''
end
end
end
Liquid::Template.register_tag('platform', Jekyll::PlatformBlock)

View file

@ -0,0 +1,50 @@
# 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 'json'
class TagScreenshotViewer < Liquid::Block
def render(context)
site = context.registers[:site]
data = JSON.parse(super)
viewer_html = '<div class="screenshot-viewer">'
viewer_html += '<div class="screenshot-viewer__tabs js-screenshot-tabs">'
data['platforms'].each do |platform|
viewer_html += "<h4 data-platform=\"#{platform['hw']}\">#{platform['hw']}</h4>"
end
viewer_html += '</div>'
viewer_html += '<div class="screenshot-viewer__screenshots">'
data['platforms'].each do |platform|
viewer_html += "<div class=\"screenshot-viewer__platform\" data-platform=\"#{platform['hw']}\">"
image_url = make_image_url(data, platform)
viewer_html += "<img src=\"#{site.config['asset_path']}#{image_url}\" class=\"pebble-screenshot pebble-screenshot--#{platform['wrapper']}\" />"
viewer_html += '</div>'
end
viewer_html += '</div>'
viewer_html += '</div>'
viewer_html
end
private
def make_image_url(data, platform)
File.dirname(data['image']) + '/' + File.basename(data['image'], File.extname(data['image'])) +
"~#{platform['hw']}" + File.extname(data['image'])
end
end
Liquid::Template.register_tag('screenshot_viewer', TagScreenshotViewer)