Generate a zip file of offline docs and a Dash/Zeal docset. (#24244)

This generates a zip file containing all of the docs, and uploads it when we publish docs, as well as a
Dash/Zeal docset that contains a feed of the docs.

Addresses at least part of #9955
diff --git a/dev/bots/docs.sh b/dev/bots/docs.sh
index 144a78c..6a3088e 100755
--- a/dev/bots/docs.sh
+++ b/dev/bots/docs.sh
@@ -30,6 +30,42 @@
   echo "$(cd -P "$(dirname "$script_location")" >/dev/null && pwd)"
 }
 
+# Zip up the docs so people can download them for offline usage.
+function create_offline_zip() {
+  # Must be run from "$FLUTTER_ROOT/dev/docs"
+  echo "Zipping Flutter offline docs archive."
+  rm -rf flutter.docs.zip doc/offline
+  (cd ./doc; zip -r -9 -q ../flutter.docs.zip .)
+}
+
+# Generate the docset for Flutter docs for use with Dash, Zeal, and Velocity.
+function create_docset() {
+  # Must be run from "$FLUTTER_ROOT/dev/docs" Must have dashing installed: go
+  # get -u github.com/technosophos/dashing Dashing produces a LOT of output
+  # (~30MB), so we redirect it, and just show the end of it if there was a
+  # problem.
+  echo "Building Flutter docset."
+  rm -rf flutter.docset
+  sed -e "s/{{VERSION}}/$FLUTTER_VERSION/g" dashing.json.tmpl > dashing.json && \
+  (dashing build --source ./doc --config ./dashing.json > /tmp/dashing.log 2>&1 || (tail -100 /tmp/dashing.log; false)) && \
+  tar cf flutter.docset.tar.gz --use-compress-program="gzip --best" flutter.docset
+}
+
+function move_offline_into_place() {
+  # Must be run from "$FLUTTER_ROOT/dev/docs"
+  echo "Moving offline data into place."
+  mkdir -p doc/offline
+  mv flutter.docs.zip doc/offline/flutter.docs.zip
+  du -sh doc/offline/flutter.docs.zip
+  if [[ "$CIRRUS_BRANCH" == "stable" ]]; then
+    echo -e "<entry>\n  <version>${FLUTTER_VERSION}</version>\n  <url>https://docs.flutter.io/offline/flutter.docset.tar.gz</url>\n</entry>" > doc/offline/flutter.xml
+  else
+    echo -e "<entry>\n  <version>${FLUTTER_VERSION}</version>\n  <url>https://master-docs-flutter-io.firebaseapp.com/offline/flutter.docset.tar.gz</url>\n</entry>" > doc/offline/flutter.xml
+  fi
+  mv flutter.docset.tar.gz doc/offline/flutter.docset.tar.gz
+  du -sh doc/offline/flutter.docset.tar.gz
+}
+
 # So that users can run this script from anywhere and it will work as expected.
 SCRIPT_LOCATION="$(script_location)"
 # Sets the Flutter root to be "$(script_location)/../..": This script assumes
@@ -53,12 +89,13 @@
 
 # Make sure dart is installed by invoking flutter to download it.
 "$FLUTTER" --version
+FLUTTER_VERSION=$(cat "$FLUTTER_ROOT/version")
 
 # If the pub cache directory exists in the root, then use that.
 FLUTTER_PUB_CACHE="$FLUTTER_ROOT/.pub-cache"
 if [[ -d "$FLUTTER_PUB_CACHE" ]]; then
-  # This has to be exported, because pub interprets setting it
-  # to the empty string in the same way as setting it to ".".
+  # This has to be exported, because pub interprets setting it to the empty
+  # string in the same way as setting it to ".".
   export PUB_CACHE="${PUB_CACHE:-"$FLUTTER_PUB_CACHE"}"
 fi
 
@@ -72,6 +109,17 @@
 (cd "$FLUTTER_ROOT" && "$DART" "$FLUTTER_ROOT/dev/tools/dartdoc.dart")
 (cd "$FLUTTER_ROOT" && "$DART" "$FLUTTER_ROOT/dev/tools/java_and_objc_doc.dart")
 
+# Zip up the docs so people can download them for offline usage.
+(cd "$FLUTTER_ROOT/dev/docs"; create_offline_zip)
+
+# Generate the docset for Flutter docs for use with Dash, Zeal, and Velocity.
+(cd "$FLUTTER_ROOT/dev/docs"; create_docset)
+
+# Move the offline archives into place, now that all the processing of the doc
+# directory is done. This avoids the tools recursively processing the archives
+# as part of their process.
+(cd "$FLUTTER_ROOT/dev/docs"; move_offline_into_place)
+
 # Ensure google webmaster tools can verify our site.
 cp "$FLUTTER_ROOT/dev/docs/google2ed1af765c529f57.html" "$FLUTTER_ROOT/dev/docs/doc"
 
diff --git a/dev/ci/docker_linux/Dockerfile b/dev/ci/docker_linux/Dockerfile
index 461220d..8f0e546 100644
--- a/dev/ci/docker_linux/Dockerfile
+++ b/dev/ci/docker_linux/Dockerfile
@@ -21,6 +21,7 @@
   git \
   wget \
   curl \
+  zip \
   unzip \
   ca-certificates \
   gnupg
@@ -34,6 +35,7 @@
 # Install the rest of the dependencies.
 RUN apt-get install -y --no-install-recommends \
   locales \
+  golang \
   ruby \
   ruby-dev \
   nodejs \
@@ -82,13 +84,22 @@
 ENV PATH="/usr/bin:${PATH}"
 RUN dpkg-query -L nodejs
 # Install Firebase
-RUN /usr/bin/npm install -g git://github.com/firebase/firebase-tools.git
+# This is why we need nodejs installed.
+RUN /usr/bin/npm --verbose install -g firebase-tools
+
+# Install dashing
+# This is why we need golang installed.
+RUN mkdir -p /opt/gopath/bin
+ENV GOPATH=/opt/gopath
+ENV PATH="${GOPATH}/bin:${PATH}"
+RUN go get -u github.com/technosophos/dashing
 
 # Set locale to en_US
 RUN locale-gen en_US "en_US.UTF-8" && dpkg-reconfigure locales
 ENV LANG en_US.UTF-8
 
 # Install coveralls and Firebase
+# This is why we need ruby installed.
 RUN gem install coveralls
 RUN gem install bundler
 
diff --git a/dev/docs/dashing.json.tmpl b/dev/docs/dashing.json.tmpl
new file mode 100644
index 0000000..09ade88
--- /dev/null
+++ b/dev/docs/dashing.json.tmpl
@@ -0,0 +1,91 @@
+{
+    "name": "flutter",
+    "package": "flutter",
+    "version": "{{VERSION}}",
+    "author": {
+      "name": "The Flutter Team",
+      "link": "https://flutter.io"
+    },
+    "index": "index.html",
+    "selectors": {
+        "#exceptions span.name a": {
+            "type": "Exception"
+        },
+
+        "body > main > div.col-xs-12.col-sm-9.col-md-8.main-content > h1": {
+            "requiretext": " library",
+            "type": "Library",
+            "regexp": " library",
+            "replacement": ""
+        },
+
+        "body > main > div.col-xs-12.col-sm-9.main-content > h1": {
+            "requiretext": " class",
+            "type": "Class",
+            "regexp": " class",
+            "replacement": ""
+        },
+
+        "body > main > div.col-xs-12.col-md-8.main-content > h1": {
+            "requiretext": " function",
+            "type": "Function",
+            "regexp": " function",
+            "replacement": ""
+        },
+
+        "body > main > div.col-sm-9.col-md-8.main-content > h1": {
+            "requiretext": " typedef",
+            "type": "Type",
+            "regexp": " typedef",
+            "replacement": ""
+        },
+
+        "body > main > .col-xs-12.col-sm-9.col-md-8.main-content > h1": {
+            "requiretext": " enum",
+            "type": "Enum",
+            "regexp": " enum",
+            "replacement": ""
+        },
+
+        "body > main > .col-md-8.main-content > h1": {
+            "requiretext": " constant",
+            "type": "Constant",
+            "regexp": " constant",
+            "replacement": ""
+        },
+
+        "body > main > div.col-xs-12.col-sm-9 > h1": {
+            "requiretext": " method",
+            "type": "Method",
+            "regexp": " method",
+            "replacement": ""
+        },
+
+        "body > main > .col-xs-12.col-sm-9.col-md-8 > h1": {
+            "requiretext": " property",
+            "type": "Property",
+            "regexp": " property",
+            "replacement": ""
+        },
+
+        "body > main > .col-xs-12.col-md-8 > h1": {
+            "requiretext": " constructor",
+            "type": "Constructor",
+            "regexp": " constructor",
+            "replacement": ""
+        },
+
+        "body > main > .col-xs-12.col-sm-9.main-content > h1": {
+            "requiretext": "operator ",
+            "type": "Operator",
+            "regexp": "operator ",
+            "replacement": ""
+        }
+    },
+    "ignore": [
+        "ABOUT"
+    ],
+    "icon32x32": "icon.png",
+    "allowJS": true,
+    "ExternalURL": "https://docs.flutter.io"
+}