Browse Source

New docs: Lit

As requested in:

* https://trello.com/c/PVnfdeaN#comment-665fffaa2798a59513617915
* https://trello.com/c/PVnfdeaN#comment-60a7c84181482c8dce7d0f55

In a period of one year, it went from 17k to 20k stars on GitHub.

This is my first time contributing to devdocs, it took me an entire day
to learn how to write a new scraper and new filters for this
documentation, including the time spent fine-tuning the result.
Denilson Sá Maia 1 month ago
parent
commit
8d27cd6c29

+ 1 - 0
assets/stylesheets/application.css.scss

@@ -82,6 +82,7 @@
         'pages/kubernetes',
         'pages/laravel',
         'pages/liquid',
+        'pages/lit',
         'pages/love',
         'pages/lua',
         'pages/gnu_make',

+ 52 - 0
assets/stylesheets/pages/_lit.scss

@@ -0,0 +1,52 @@
+._lit {
+  @extend %simple;
+
+  h4 { @extend %block-label, %label-blue; }
+
+  .propertyDetails {
+    padding-left:1.5em
+  }
+  .heading.property {
+    margin-top:2em
+  }
+  .heading.property > h4 {
+    font-weight:400
+  }
+  .newKeyword,
+  .readonlyKeyword,
+  .staticKeyword {
+    font-style:italic
+  }
+  .functionName,
+  .propertyName {
+    font-weight:700
+  }
+  aside.litdev-aside {
+    display: flex;
+    border-style: solid;
+    border-width: 1px;
+    padding: 1em 1em 1em 0em;
+    margin: 1em 0;
+    svg {
+      width: 1.5em;
+      margin-inline: 1em;
+    }
+  }
+  litdev-switchable-sample {
+    pre[data-language] {
+      position: relative;
+    }
+    pre[data-language]::before {
+      position: absolute;
+      top: 0;
+      right: 16px;
+      opacity: 0.5;
+    }
+    pre[data-language="js"]::before {
+      content: "JavaScript";
+    }
+    pre[data-language="ts"]::before {
+      content: "TypeScript";
+    }
+  }
+}

+ 71 - 0
lib/docs/filters/lit/clean_html.rb

@@ -0,0 +1,71 @@
+module Docs
+  class Lit
+    class CleanHtmlFilter < Filter
+      def call
+
+        css('.offscreen, #inlineToc, a.anchor, [aria-hidden="true"], #prevAndNextLinks').remove
+
+        css('[tabindex]').remove_attribute('tabindex')
+
+        # Removing the side navigation.
+        css('#docsNavWrapper, #rhsTocWrapper').remove
+
+        # Removing this extra div.
+        div = at_css('#articleWrapper')
+        article = div.at_css('article')
+        article.remove_attribute('id')
+        div.replace(article)
+
+        # Expanding and replacing the <template>, statically.
+        # This code is a hacky incomplete implementation of
+        # https://github.com/lit/lit.dev/blob/main/packages/lit-dev-content/src/components/litdev-aside.ts
+        css('litdev-aside').each do |node|
+          frag = Nokogiri::HTML::DocumentFragment.new(node.document)
+          template = node.at_css('template')
+          aside = template.children.first
+          aside['class'] = 'litdev-aside'
+          frag.add_child(aside)
+          template.remove
+          div = Nokogiri::XML::Node.new('div', @doc)
+          div.add_child(node.children)
+          aside.add_child(div)
+          node.replace(aside)
+        end
+
+        # Removing the live playground examples.
+        # https://github.com/lit/lit.dev/blob/main/packages/lit-dev-content/src/components/litdev-example.ts
+        # Someday we can try enabling the live examples by adding appropriate code to assets/javascripts/views/pages/.
+        css('litdev-example').each do |node|
+          node.remove
+        end
+
+        # Cleaning up the preformatted example code.
+        css('pre:has(code[class])').each do |node|
+          lang = node.at_css('code')['class']
+          lang.sub! /^language-/, ''
+          node.content = node.css('.cm-line').map(&:content).join("\n")
+          node['data-language'] = lang
+        end
+
+        # Cleaning up example import.
+        css('div.import').each do |node|
+          pre = Nokogiri::XML::Node.new('pre', @doc)
+          pre.content = node.css('.cm-line').map(&:content).join("\n")
+          pre['data-language'] = 'javascript'
+          node.replace(pre)
+        end
+
+        # Moving the "kind" to inside the header.
+        # Because it looks better this way.
+        css('.kindTag').each do |kindtag|
+          heading = kindtag.parent
+          next unless heading['class'].include? 'heading'
+          h = heading.at_css('h2, h3, h4')
+          h.prepend_child(kindtag)
+        end
+
+        doc
+      end
+    end
+  end
+end

+ 72 - 0
lib/docs/filters/lit/entries.rb

@@ -0,0 +1,72 @@
+module Docs
+  class Lit
+    class EntriesFilter < Docs::EntriesFilter
+
+      def get_name
+        name = at_css('h1').content.strip
+      end
+
+      def get_type
+        # The type/category is section name from the sidebar.
+        active = at_css('#docsNav details li.active')
+        return nil unless active
+        summary = active.ancestors('details').first.at_css('summary')
+        return nil unless summary
+        summary.css('[aria-hidden="true"]').remove
+        summary.content.strip
+      end
+
+      def additional_entries
+        entries = []
+
+        # Code for the API reference pages (and other similar pages).
+        scope_name = ''
+        css('.heading > h2[id], .heading > h3[id], .heading > h4[id]').each do |node|
+          name = node.content.strip
+          id = node['id']
+          # The kindTag has these values:
+          # class, decorator, directive, function, namespace, type, value
+          kind = node.parent.at_css('.kindTag')&.content&.strip
+
+          if kind
+            # Saving the current "scope", i.e. the current class name.
+            # This is useful to prefix the method/property names, which are defined after this element.
+            scope_name = name
+            name = kind + " " + name
+          else
+            # If this is a method/property, it has a different markup.
+            # Let's extract them and add a prefix for disambiguation.
+            function = node.at_css('.functionName')
+            property = node.at_css('.propertyName')
+            if function
+              # Note how "functions" are actually "methods" of some class.
+              # Bare (top-level) functions are extracted when `.kindTag` is "function".
+              name = scope_name + '.' + function.content.strip
+              kind = 'method'
+            elsif property
+              name = scope_name + '.' + property.content.strip
+              kind = 'property'
+            end
+          end
+
+          # If we couldn't figure out the kind, this is a header tag that we can ignore.
+          entries << [name, id, kind] if kind
+        end
+
+        # Code for the Built-in Directives page.
+        # This page has a TOC of the built-in directives, with a clear documentation of each one.
+        # Note that the directives are also indexed in the API reference pages.
+        # Yes, each directive is indexed twice, because each one is documented twice.
+        css('.directory a[href^="#"]').each do |node|
+          name = node.content.strip
+          id = node['href'].sub /^#/, ''
+          # type will be "Built-in directives"
+          type = node.ancestors('article').at_css('h1').content.strip
+          entries << [name, id, type]
+        end
+
+        entries
+      end
+    end
+  end
+end

+ 55 - 0
lib/docs/scrapers/lit.rb

@@ -0,0 +1,55 @@
+module Docs
+  class Lit < UrlScraper
+    self.name = 'Lit'
+    self.slug = 'lit'
+    self.type = 'lit'
+
+    self.links = {
+      home: 'https://lit.dev/',
+      code: 'https://github.com/lit/lit/'
+    }
+
+    options[:container] = 'main'
+
+    options[:max_image_size] = 250_000
+
+    # Note: the copyright will change soon due to https://lit.dev/blog/2025-10-14-openjs/
+    options[:attribution] = <<-HTML
+      &copy; Google LLC<br>
+      Licensed under the Creative Commons Attribution 3.0 Unported License.
+    HTML
+
+    options[:fix_urls] = ->(url) do
+      # A name without any extension is assumed to be a directory.
+      # example.com/foobar -> example.com/foobar/
+      url.sub! /(\/[-a-z0-9]+)([#?]|$)/i, '\1/\2'
+
+      url
+    end
+
+    # The order of the filters is important.
+    # The entries filter is applied to the raw (mostly) unmodified HTML.
+    # The clean_html filter reformats the HTML to the a more appropriate markup for devdocs.
+    html_filters.push 'lit/entries', 'lit/clean_html'
+
+    version '3' do
+      self.release = '3.3.1'
+      self.base_url = 'https://lit.dev/docs/'
+      options[:skip_patterns] = [/v\d+\//]
+    end
+
+    version '2' do
+      self.release = '2.8.0'
+      self.base_url = 'https://lit.dev/docs/v2/'
+    end
+
+    version '1' do
+      self.release = '1.0.1'
+      self.base_url = 'https://lit.dev/docs/v1/'
+    end
+
+    def get_latest_version(opts)
+      get_npm_version('lit', opts)
+    end
+  end
+end

BIN
public/icons/docs/lit/16.png


BIN
public/icons/docs/lit/16@2x.png


+ 3 - 0
public/icons/docs/lit/SOURCE

@@ -0,0 +1,3 @@
+https://github.com/lit/lit.dev/blob/main/packages/lit-dev-content/site/images/flame.svg
+https://github.com/lit/lit.dev/blob/main/packages/lit-dev-content/site/images/icon.svg
+https://github.com/lit/lit/blob/main/packages/lit/logo.svg