# coding: utf-8 require "pathname" require "set" require "redcarpet" require "rouge" require "rouge/plugins/redcarpet" module SassHelpers def page_title title = "Sass: " if current_page.data.title title << current_page.data.title else title << "Syntactically Awesome Style Sheets" end title end def copyright_years(start_year) end_year = Date.today.year if start_year == end_year start_year.to_s else start_year.to_s + '–' + end_year.to_s end end def pages_for_group(group_name) group = data.nav.find do |g| g.name == group_name end pages = [] return pages unless group if group.directory pages << sitemap.resources.select { |r| r.path.match(%r{^#{group.directory}}) && !r.data.hidden }.map do |r| ::Middleman::Util.recursively_enhance({ :title => r.data.title, :path => r.url }) end.sort_by { |p| p.title } end pages << group.pages if group.pages pages.flatten end def without_html(page) url_for(page).sub(/\.html$/, '') end def typedoc? current_page.url.start_with?("/documentation/js-api/") end def documentation_toc _toc_level(nil, data.documentation.toc) end def _toc_level(parent_href, links) if parent_href overview = content_tag(:li, content_tag(:a, "Overview", href: parent_href, class: ("selected" if current_page.url == parent_href + ".html")), class: "overview") end content_tag(:ul, [ overview, *links.map do |link| children = link[:children] text = link.keys.reject {|k| k == :children}.first href = link[text] content_tag(:li, [ content_tag(:a, text, href: href, class: [ ("section" if children), ("open selected" if current_page.url.start_with?(href)) ].compact.join(" ")), (_toc_level(href, children) if children) ].compact) end ].compact) end # Renders a code example. # # This takes a block of SCSS and/or indented syntax code, and emits HTML that # (combined with JS) will allow users to choose which to display. # # The SCSS should be separated from the Sass with `===`. For example, in Haml: # # - example do # :plain # .foo { # color: blue; # } # === # .foo # color: blue # # Different sections can be separated within one syntax (for example, to # indicate different files) with `---`. For example, in Haml: # # - example do # :plain # // _reset.scss # * {margin: 0} # --- # // base.scss # @import 'reset'; # === # // _reset.sass # * # margin: 0; # --- # // base.sass # @import reset # # Padding is added to the bottom of each section to make it the same length as # the section in the other language. # # A third section may optionally be provided to represent compiled CSS. If # it's not passed and `autogen_css` is `true`, it's generated from the SCSS # source. If the autogenerated CSS is empty, it's omitted entirely. # # If `syntax` is either `:sass` or `:scss`, the first section will be # interpreted as that syntax and the second will be interpreted (or # auto-generated) as the CSS output. def example(autogen_css: true, syntax: nil, &block) contents = _capture(&block) if syntax == :scss scss, css = contents.split("\n===\n") elsif syntax == :sass sass, css = contents.split("\n===\n") else scss, sass, css = contents.split("\n===\n") throw ArgumentError.new("Couldn't find === in:\n#{contents}") if sass.nil? end scss_sections = scss ? scss.split("\n---\n").map(&:strip) : [] sass_sections = sass ? sass.split("\n---\n").map(&:strip) : [] if css.nil? && autogen_css sections = scss ? scss_sections : sass_sections if sections.length != 1 throw ArgumentError.new( "Can't auto-generate CSS from more than one SCSS file.") end css = Sass::Engine.new( sections.first, syntax: syntax || :scss, style: :expanded ).render css = nil if css.empty? end css_sections = css ? css.split("\n---\n").map(&:strip) : [] # Calculate the lines of padding to add to the bottom of each section so # that it lines up with the same section in the other syntax. scss_paddings = [] sass_paddings = [] css_paddings = [] max_num_sections = [scss_sections, sass_sections, css_sections].map(&:length).max max_num_sections.times do |i| scss_section = scss_sections[i] sass_section = sass_sections[i] css_section = css_sections[i] scss_lines = (scss_section || "").lines.count sass_lines = (sass_section || "").lines.count css_lines = (css_section || "").lines.count # Whether the current section is the last section for the given syntax. last_scss_section = i == scss_sections.length - 1 last_sass_section = i == sass_sections.length - 1 last_css_section = i == css_sections.length - 1 # The maximum lines for any syntax in this section, ignoring syntaxes for # which this is the last section. max_lines = [ last_scss_section ? 0 : scss_lines, last_sass_section ? 0 : sass_lines, last_css_section ? 0 : css_lines ].max scss_paddings << if last_scss_section # Make sure the last section has as much padding as all the rest of # the other syntaxes' sections. _total_padding(sass_sections[i..-1], css_sections[i..-1]) - scss_lines - 2 elsif max_lines > scss_lines max_lines - scss_lines end sass_paddings << if last_sass_section _total_padding(scss_sections[i..-1], css_sections[i..-1]) - sass_lines - 2 elsif max_lines > sass_lines max_lines - sass_lines end css_paddings << if last_css_section _total_padding(scss_sections[i..-1], sass_sections[i..-1]) - css_lines - 2 elsif max_lines > css_lines max_lines - css_lines end end @unique_id ||= 0 @unique_id += 1 id = @unique_id ul_contents = [] ul_contents << _syntax_tab("SCSS", "scss", id, enabled: scss) if scss ul_contents << _syntax_tab("Sass", "sass", id, enabled: !scss) if sass ul_contents << _syntax_tab("CSS", "css", id) if css contents = [ content_tag(:ul, ul_contents, class: "ui-tabs-nav ui-helper-reset ui-helper-clearfix") ] if scss contents << _syntax_div("SCSS Syntax", "scss", scss_sections, scss_paddings, id, enabled: scss) end if sass contents << _syntax_div("Sass Syntax", "sass", sass_sections, sass_paddings, id, enabled: !scss) end if css contents << _syntax_div("CSS Output", "css", css_sections, css_paddings, id) end max_source_width = (scss_sections + sass_sections).map {|s| s.split("\n")}.flatten.map(&:size).max max_css_width = css_sections.map {|s| s.split("\n")}.flatten.map(&:size).max can_split = max_css_width && (max_source_width + max_css_width) < 110 if can_split if max_source_width < 55 && max_css_width < 55 split_location = 0.5 else # Put the split exactly in between the two longest lines. split_location = 0.5 + (max_source_width - max_css_width) / 110.0 / 2 end end text = content_tag(:div, contents, class: "code-example ui-tabs #{'can-split' if can_split}", "style": ("--split-location: #{split_location * 100}%" if split_location)) # Newlines between tags cause Markdown to parse these blocks incorrectly. text = text.gsub(%r{\n+<(/?[a-z0-9]+)}, '<\1') if block_is_haml?(block) haml_concat text else # Padrino's concat helper doesn't play nice with nested captures. @_out_buf << text end end # Returns the number of lines of padding that's needed to match the height of # the `
`s generated for `sections1` and `sections2`. def _total_padding(sections1, sections2) sections1 ||= [] sections2 ||= [] [sections1, sections1].map(&:length).max.times.sum do |i| # Add 2 lines to each additional section: 1 for the extra padding, and 1 # for the extra margin. [ (sections1[i] || "").lines.count, (sections2[i] || "").lines.count ].max + 2 end end # Returns the text of an example tab for a single syntax. def _syntax_tab(name, syntax, id, enabled: false) content_tag(:li, [ content_tag(:a, name, href: "#example-#{id}-#{syntax}", class: "ui-tabs-anchor") ], class: [ "ui-tabs-tab", ('css-tab' if syntax == 'css'), ('ui-tabs-active' if enabled) ].compact.join(' ')) end # Returns the text of an example div for a single syntax. def _syntax_div(name, syntax, sections, paddings, id, enabled: false) inactive = syntax == 'scss' ? '' : 'ui-tabs-panel-inactive' content_tag(:div, [ content_tag(:h3, name, class: 'visuallyhidden'), *sections.zip(paddings).map do |section, padding| padding = 0 if padding.nil? || padding.negative? _render_markdown("```#{syntax}\n#{section}#{"\n" * padding}\n```") end ], id: "example-#{id}-#{syntax}", class: [ "ui-tabs-panel", syntax, ('ui-tabs-panel-inactive' unless enabled) ].compact.join(' ')) end # Returns the version for the given implementation (`:dart`, `:ruby`, or # `:libsass`), or `nil` if it hasn't been made available yet. def impl_version(impl) data.version && data.version[impl] end # Returns the URL tag for the latest release of the given implementation. def release_url(impl) if impl == :ruby return "https://github.com/sass/ruby-sass/blob/stable/doc-src/SASS_CHANGELOG.md" end version = impl_version(impl) repo = case impl when :dart; "dart-sass" when :migrator; "migrator" when :libsass; "libsass" end if version "https://github.com/sass/#{repo}/releases/tag/#{version}" else "https://github.com/sass/#{repo}/releases" end end # Returns HTML for a warning. # # The contents should be supplied as a block. def heads_up _concat(content_tag :div, [ content_tag(:h3, '⚠️ Heads up!'), _render_markdown(_capture {yield}) ], class: 'sl-c-callout sl-c-callout--warning') end # Returns HTML for a fun fact that's not directly relevant to the main # documentation. # # The contents should be supplied as a block. def fun_fact _concat(content_tag :div, [ content_tag(:h3, '💡 Fun fact:'), _render_markdown(_capture {yield}) ], class: 'sl-c-callout sl-c-callout--fun-fact') end def table_of_contents(resource) content = File.read(resource.source_file) toc_renderer = Redcarpet::Render::HTML_TOC.new markdown = Redcarpet::Markdown.new(toc_renderer) markdown.render(content) end def markdown_wrap(content) Tilt['markdown'].new { content }.render end # Renders a status dashboard for each implementation's support for a feature. # # Each implementation's value can be: # # * `true`, indicating that that implementation fully supports the feature; # * `false`, indicating that it does not yet support the feature at all; # * `:partial`, indicating that it has limited or incorrect support for the # feature; # * or a string, indicating the version it started supporting the feature. # # When possible, prefer using the start version rather than `true`. # # If `feature` is passed, it should be a terse (one- to three-word) # description of the particular feature whose compatibility is described. This # should be used whenever the status isn't referring to the entire feature # being described by the surrounding prose. # # This takes an optional Markdown block that should provide more information # about the implementation differences or the old behavior. def impl_status(dart: nil, libsass: nil, ruby: nil, node: nil, feature: nil) compatibility = feature ? "Compatibility (#{feature}):" : "Compatibility:" contents = [content_tag(:div, compatibility, class: "compatibility")] contents << _impl_status_row('Dart Sass', dart) unless dart.nil? contents << _impl_status_row('LibSass', libsass) unless libsass.nil? contents << _impl_status_row('Node Sass', node) unless node.nil? contents << _impl_status_row('Ruby Sass', ruby) unless ruby.nil? if block_given? # The no-op href here ensures that this toggle is focusable in browsers. contents << content_tag(:div, content_tag(:a, '▶', href: 'javascript:;')) end _concat(content_tag(:dl, contents, class: 'impl-status sl-c-description-list sl-c-description-list--horizontal')) if block_given? # Get rid of extra whitespace to avoid more bogustags. _concat(content_tag :div, _render_markdown(_capture {yield}).strip, class: 'sl-c-callout sl-c-callout--impl-status') end end # Renders a single row for `impl_status`. def _impl_status_row(name, status) status_text = if status == true "✓" elsif status == false "✗" elsif status == :partial "partial" else "since #{status}" end content_tag :div, [ content_tag(:dt, name), content_tag(:dd, status_text), ] end # Renders API docs for a Sass function (or mixin). # # The function's name is parsed from the signature. The API description is # passed as a Markdown block. If `returns` is passed, it's included as the # function's return type. # # Multiple signatures may be passed, in which case they're all included in # sequence. def function(*signatures, returns: nil) names = Set.new highlighted_signatures = signatures.map do |signature| name, rest = signature.split("(", 2) name_without_namespace = name.split(".").last html = Nokogiri::HTML(_render_markdown(<
#{return_type_link(returns)}", class: 'c1') end html = content_tag :div, [ content_tag(:pre, [ # Make sure there's no whitespace between these two, since they're in a # . content_tag(:a, '', class: 'anchor', href: "##{names.first}") + content_tag(:code, merged_signatures) ], class: 'signature highlight scss'), _render_markdown(_capture {yield}) ], class: 'sl-c-callout sl-c-callout--function', id: names.first _concat(names.uniq[1..-1].inject(html) {|h, n| content_tag(:div, h, id: n)}) end def return_type_link(return_type) return_type.split("|").map do |type| type = type.strip case type.strip when 'number'; link_to type, '/documentation/values/numbers' when 'string'; link_to type, '/documentation/values/strings' when 'quoted string'; link_to type, '/documentation/values/strings#quoted' when 'unquoted string'; link_to type, '/documentation/values/strings#unquoted' when 'color'; link_to type, '/documentation/values/colors' when 'list'; link_to type, '/documentation/values/lists' when 'map'; link_to type, '/documentation/values/maps' when 'boolean'; link_to type, '/documentation/values/booleans' when 'null'; link_to 'null
', '/documentation/values/null' when 'function'; link_to type, '/documentation/values/functions' when 'selector'; link_to type, '/documentation/modules/selector#selector-values' else raise "Unknown type #{type}" end end.join(" | ") end # Removes leading spaces from every non-empty line in `text` while preserving # relative indentation. def remove_leading_indentation(text) text.gsub(/^#{text.scan(/^ *(?=\S)(?!<)/).min}/, "") end # A helper method that renders a chunk of Markdown text. def _render_markdown(content) @redcarpet ||= Redcarpet::Markdown.new( Class.new(Redcarpet::Render::HTML) { include Rouge::Plugins::Redcarpet }, markdown ) find_and_preserve(@redcarpet.render(content)) end # Captures the contents of `block` from ERB or Haml. # # Strips all leading indentation from the block. def _capture(&block) remove_leading_indentation( (block_is_haml?(block) ? capture_haml(&block) : capture(&block)) || "") end # Concatenates `text` to the document. # # Converts all newlines to spaces in order to avoid weirdness when rendered # HTML is nested within Markdown. Adds newlines before and after the content # to ensure that it doesn't cause adjacent markdown not to be parsed. def _concat(text) concat("\n\n" + text.gsub("\n", " ") + "\n\n") end end