Skip to content

Instantly share code, notes, and snippets.

@lo48576
Last active February 28, 2020 08:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lo48576/f331b8d164573006ec129e73514bd735 to your computer and use it in GitHub Desktop.
Save lo48576/f331b8d164573006ec129e73514bd735 to your computer and use it in GitHub Desktop.
XSL filter for my blog (at 2018-12-13), with XSL dependency tracking

Customized XSL filter for nanoc

Features

  • Track dependency files for each layout XSL files.
  • Use correct base path for imports / includes
    • Current nanoc (4.11) uses the current directory (project root) for base path of XSLT includes / imports. href.content = (xsl_dir + href).relative_path_from(current_dir).to_s fixes this.

Known problems

No cache for dependencies

This filter doesn't cache deps for files. Ideally, it is enough to calculate deps only once for each XSL files.

No cache for ::Nokogiri::{XML,XSLT::Stylesheet}

I don't know whether they should be cached or not, but it might be inefficient to create and edit XML / XSLT objects each time the filter runs...

Ignoring @xml:base

@xml:base changes base path for any external references, so it should affect path resolution. I hope very few people use this attribute...

Ignoring document()

document() function of XSLT can cause additional dependencies to other external files. This wuold be impossible to track correctly, becuase it can be invoked with runtime value. In case document() is used (with dynamic argument), the filter should fallback to the mechanism of the current release, i.e. the filter should consider that all XSLT templates are outdated.

Using internal API without knowledge

I (implementer) don't know about nanoc's internal, but using _context.dependency_tracker.bounce. I'm not sure it is really right way.

Non-idiomatic Ruby?

I'm not Rubyist, and this code won't be beautiful enough...

# Example.
compile '/articles/**/*.xml' do
# I didn't know, but multiple `layout`s can be specified...
layout '/xsl/blog-cardina1-red.xsl'
layout '/article.xhtml'
end
layout '/xsl/**/*.xsl', :larry_xsl
layout '/**/*', :erb, trim_mode: '-'
module Larry
XSLT_NAMESPACE = "http://www.w3.org/1999/XSL/Transform"
XSLT_DEP_ATTRS = "(/xsl:stylesheet/xsl:import | /xsl:stylesheet/xsl:include)/@href"
class XslImporter
attr_reader :filter
def initialize(filter)
@filter = filter
end
# Returns direct dependencies by relative paths from the layouts directory.
def get_direct_deps(layout_dir, identifier)
dirname = Pathname.new("#{layout_dir}#{filter.layouts[identifier].identifier}").dirname
content = filter.layouts[identifier].raw_content
xml = ::Nokogiri::XML(content)
# TODO: Deal with `@base` attr.
xml.xpath(XSLT_DEP_ATTRS, 'xsl': XSLT_NAMESPACE).map do |attr|
href = attr.content
abs_path = (dirname + href).cleanpath
"/#{abs_path.relative_path_from(layout_dir)}"
end
end
# Returns all dependencies including indirect ones by relative paths from the layouts directory.
#
# Returned array does not contain the given `identifier` itself
# (if there are no circular dependencies).
def get_all_deps(layout_dir, identifier)
unchecked_deps = get_direct_deps(layout_dir, identifier)
checked_deps = []
loop do
checking = unchecked_deps.pop
break unless checking
get_direct_deps(layout_dir, checking).each do |direct|
next if checked_deps.include?(direct) || unchecked_deps.include?(direct)
unchecked_deps << direct
end
checked_deps << checking
end
checked_deps
end
end
# See <https://github.com/nanoc/nanoc/blob/4.9.4/nanoc/lib/nanoc/filters/xsl.rb>
class XSL < Nanoc::Filter
identifier :larry_xsl
requires 'nokogiri'
# Runs the item content through an [XSLT](http://www.w3.org/TR/xslt)
# stylesheet using [Nokogiri](http://nokogiri.org/).
#
# @param [String] _content Ignored. As the filter can be run only as a
# layout, the value of the `:content` parameter passed to the class at
# initialization is used as the content to transform.
#
# @param [Hash] params The parameters that will be stored in corresponding
# `xsl:param` elements.
#
# @return [String] The transformed content
def run(_content, params = {})
Nanoc::Extra::JRubyNokogiriWarner.check_and_warn
if assigns[:layout].nil?
raise 'The XSL filter can only be run as a layout'
end
layout = assigns[:layout]
xsl_path = layout.identifier
if xsl_path.nil?
raise 'XSL path is not specified'
end
current_dir = Pathname.pwd
layout_dir = current_dir + 'layouts'
xsl_dir = Pathname.new("#{layout_dir}#{xsl_path}").dirname
importer = XslImporter.new(self)
importer.get_all_deps(layout_dir, xsl_path).each do |dep|
# TODO: I don't know what parameters to be passed to `bounce`.
layout._context.dependency_tracker.bounce(@layouts[dep]._unwrap)
end
xsl_xml = ::Nokogiri::XML(layout.raw_content)
# TODO: Deal with `@base` attr.
xsl_xml.xpath(XSLT_DEP_ATTRS, 'xsl': XSLT_NAMESPACE).each do |href|
href.content = (xsl_dir + href).relative_path_from(current_dir).to_s
end
# TODO: Cache this `xsl` or `xsl_xml`.
xsl = ::Nokogiri::XSLT::Stylesheet.parse_stylesheet_doc(xsl_xml)
parse_opts = ::Nokogiri::XML::ParseOptions::new.strict.norecover.nonoent
xml = ::Nokogiri::XML(assigns[:content], nil, nil, parse_opts)
xsl.apply_to(xml, ::Nokogiri::XSLT.quote_params(params))
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment