使用NodeJS 生成Vue中文版 docSet 離線文檔

用法

  • Mac下用戶下載 Dash 使用文檔 (下載 .docset 後綴文件,雙擊導入便可)
  • Windows 和 Linux 用戶可下載 Zeal 使用本文檔 (應該相似吧,我沒用過,自行搜一下吧,溜了溜了)

製做 docSet 文檔

Dash所需的文檔都是.docSet後綴的文件,其實docSet文件就是一個文件夾而已,裏頭包含最終的html文檔,以及根據html創建的索引(索引放在sqlite數據庫中)。php

生成文檔的方法有不少種,如 PythonRubyObjective-CNode.jsPHPcss

能夠選擇 鏡像時處理,也能夠鏡像後處理。只須要結果中包含html,以及sqlite 就OJBK。html

這裏我選擇 鏡像後用NodeJS處理vue

須要用到的庫爲:java

  • fs 作一些文件的讀寫操做
  • path 路徑處理
  • sync-exec 執行一些cmd命令
  • sqlite-sync 作一些 sqlite 操做
  • cheerio 服務器版的jQuery

主要步驟:

根據官網提供的官方文檔,整個轉換主要有如下5個步驟:node

1. 建立Docset目錄結構(Create the Docset Folder);
2. 複製HTML文件(Copy the HTML Documentation);
3. 建立Info.plist文件(Create the Info.plist File);
4. 建立SQLite數據庫文件(Create the SQLite Index);
5. 寫入SQLite數據索引(Populate the SQLite Index);
複製代碼

1. 鏡像站點

鏡像工具備不少,這裏只推薦我嘗試過的幾款,而且用着還不錯:git

名稱 平臺 地址
HTTrack OS X / Windows/Linux/Unix/BSD www.httrack.com
SiteSucker OS X sitesucker.us/home.html
Cyotek WebCopy Windows www.cyotek.com/cyotek-webc…

實際上這幾種都不是很完美,或多或少會漏一些外部站點的資源文件,目前還沒找到解決辦法,github

若是大家有解決辦法,麻煩**@**我一下。web

我這裏以SiteSucker 爲例,鏡像 https://cn.vuejs.org ,一級目錄結構以下:sql

// cn.vuejs.org
├── _00.txt
├── _downloads.html
├── coc
├── css
├── fonts
├── guide
├── images
├── index.html
├── js
├── manifest.json
├── support-vuejs
└── v2
複製代碼

重點關注對象爲如下,提取其中的內容,生成索引

  • cn.vuejs.org/v2/api/index.html (API 列表)
  • cn.vuejs.org/v2/guide/* (官網教程列表)
  • cn.vuejs.org/v2/style-guide/index.html (風格指南)

參考Dash的Vue文檔,一層層撥開後,發現官方用的HTTrack作的鏡像,而且資源文件比我本身用HTTrack鏡像下來的資源齊全,一番折騰下來,也沒成功。

對比後選擇用官方的外部資源,文檔內容則用本身鏡像的

左側爲dash官方文檔中的資源 、中間爲合併後的資源(後面要用到)、 右側爲本身鏡像的資源
複製代碼

2. 建立Docset目錄結構(Create the Docset Folder)

本例中咱們建立的文檔叫 VueJS,因此按下列結構建立目錄:

mkdir -p VueJS.docset/Contents/Resources/Documents/
複製代碼

3.複製HTML文件(Copy the HTML Documentation)

​ 把全部的html文檔拷貝到Documents文件夾中,dash默認 Documents 爲文檔根目錄

​ 爲了省事,我把須要的資源文件放在了當前項目下。

cp -r  ./Documents  VueJS.docset/Contents/Resources/
複製代碼

4.建立Info.plist文件(Create the Info.plist File)

在 VueJS.docset/Contents/ 中建立Info.plist文件,注意文件名的大小寫,文件內容以下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>CFBundleIdentifier</key>
        <string>VueJS</string>
        <key>CFBundleName</key>
        <string>VueJS-CN</string>
        <key>DocSetPlatformFamily</key>
        <string>VueJS</string>
        <key>isDashDocset</key>
        <true/>
        <key>DashDocSetFamily</key>
        <string>dashtoc3</string>
        <key>dashIndexFilePath</key>
        <string>cn.vuejs.org/index.html</string>
    </dict>
    </plist>
複製代碼

一個xml文件,裏面都是成對的key-string配置項

  • dashIndexFilePath 表示在Dash中點擊你的文檔後,默認的主頁是什麼
  • CFBundleName 爲在dash 中的文檔名稱
  • DashDocSetFamily 左下角顯示索引列表 (這裏我有配置,可是沒生效,後續再研究)
  • 其餘部分爲一些關鍵字

5.建立SQLite數據庫文件(Create the SQLite Index)

  1. 建立sqlite索引。

    索引文件的位置是:VueJS.docset/Contents/Resources/docSet.dsidx ,(Mac電腦已經預裝了sqlite)

    因此直接從命令行進入Resources文件夾,在命令行中敲:

    sqlite3 docSet.dsidx 
    複製代碼
  2. 這樣就進入了sqlite數據庫,接下來,建立數據表

    CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT)
    複製代碼

6.寫入SQLite數據索引(Populate the SQLite Index);

再日後就是,從html文件中提取內容,插入索引。這是最重要的一點,這裏沒弄好,整個都沒啥用。

INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES ('name', 'type', 'path');
複製代碼

其中

  • name 爲關鍵字,好比你想在dash中輸入一個select就能夠查詢,那這個select就是關鍵字;
  • type 爲關鍵字的類型,官方支持的有不少,如Class、Function、Guide、Method等;
  • path 爲文檔的錨點地址,點擊目錄跳轉

官方原文爲:

  • name is the name of the entry. For example, if you are adding a class, it would be the name of the class. This is the column that Dash searches.
  • type is the type of the entry. For example, if you are adding a class, it would be "Class". For a list of types that Dash recognises, see below.
  • path is the relative path towards the documentation file you want Dash to display for this entry. It can contain an anchor (#). Alternatively, Dash also supports http:// URL entries.

如下爲個人部分代碼,完整的代碼在 build-vue.js

// build-vue.js

/**
 * 根據各個標題處理相應的錨點 添加索引
 *
 * @param $ dom 對象
 * @param relativePath 相對路徑
 * @param dir 文件夾名稱
 */
function handleTitles($,relativePath,dir) {
    // 教程模塊 以h2 爲索引,須要添加一個h1 的索引
    let h1Title = '';
    if(dir === 'guide'){
        $('h1').each(function (i,h) {
            h1Title = Array.from(h.childNodes).map((node) => node.data ).join('');
            db.run(`INSERT INTO searchIndex (name, type, path) VALUES ('${h1Title}', '${type['guide']}', '${relativePath}')`,function(res){
                if(res.error) throw res.error;
                console.log(res);
            });
        });
    }
    $('h2').each(function (i,h) {
        if(!h.attribs.id) return;
        let h2s = extractText(h); // 提取標題中的ID、文本內容
        let h3s = [];
        if(dir === 'api'){
            h3s = collectH3s(h);
            if(h3s.length<1) return
        }
        let entryType = type[h2s.id] || type['h2'];  // 默認 Section
        console.log(h2s);
        let h2Num = dir === 'api' ? 1 : 0;
        let h2Type = type['h2'];  // h2 歸類爲 Section
        addDashAnchor(h,h2s.id,h2Type,h2Num);
        let inTitle = `${h2s.text} ${dir === 'guide' ? ' - '+ h1Title : ''}`;
        let iniType =  dir ==='api' ? type['guide'] : h2Type;
        db.run(`INSERT INTO searchIndex (name, type, path) VALUES ('${inTitle}', '${iniType}', '${relativePath}#${encodeURIComponent(h2s.id)}')`,function(res){
            if(res.error) throw res.error;
            console.log(res);
        });
        // api下 須要處理 h3 標題,生成相應的索引
        if(dir === 'api'){
            h3s.forEach(function (titleNode,index) {
                let id =  titleNode.attribs.id;
                let text = [].slice.call(titleNode.childNodes).map( (node) => node.data).join('');
                // 須要處理括號
                if(text.match(/^([^(]+)\(/)) text= text.match(/^([^(]+)\(/)[1];
                console.log(id,text,entryType);
                addDashAnchor(titleNode,id,entryType,0);
                db.run(`INSERT INTO searchIndex (name, type, path) VALUES ('${text}', '${entryType}', '${relativePath}#${encodeURIComponent(id)}')`,function(res){
                    if(res.error) throw res.error;
                    console.log(res);
                });
            });
        }
    });

    /**
     *  提取標題中的ID、文本內容
     * @param h node
     * @returns {{id: *, text: *}}   id 用來生成錨點、text當作標題
     */
    function extractText (h) {
        let title =  [].slice.call(h.childNodes).map( (node) => node.tagName === 'a' ?  node.attribs.title : '').join('');
        let id = h.attribs.id;
        return {
            id: id,
            text:  title ? htmlEscape(title) : id // 若是沒有就用ID 代替
        }
    }

    // 字符轉義
    function htmlEscape (text) {
        return text
            .replace(/&amp;/g, '&')
            .replace(/&quot;/g, '"')
            .replace(/&#39;/g, `'`)
            .replace(/&lt;/g, '<')
            .replace(/&gt;/g, '>')
    }
    // 提取h2 附近的h3 標題列表
    function collectH3s (h) {
        let h3s = [];
        let next = h.nextSibling;
        while (next && next.tagName !== 'h2') {
            if (next.tagName === 'h3') {
                next.childNodes = removeTagA(next);
                h3s.push(next)
            }
            next = next.nextSibling
        }
        return h3s
    }
    // 移除A標籤
    function removeTagA(h) {
       return [].slice.call(h.childNodes).filter(function (node) {
            return node.tagName !== 'a'
        })
    }
    // 添加dash規定格式的 錨點
    function addDashAnchor(h,name,types,num) {
        let nameStr = (`//dash_ref_${name}/${types}/${encodeURIComponent(name)}/${num}`); // 須要對URL 進行URL編碼(百分比轉義)
        let dashAnchor = `<a class="dashAnchor" name="${nameStr}"/>`;
        h.childNodes = removeTagA(h); // 取完title以後移除原有的錨點,添加 dash規定格式的錨點
        $(h).before(dashAnchor).html();
    }
}

複製代碼

7. 導入文檔

把全部的索引數據都插入到searchIndex之後,docSet文檔就製做好了,直接雙擊 VueJS.docSet就能夠導入Dash了。

截圖

以上示例代碼都可在 Gayhub 查看

參考

kapeli.com/docsets
Dash 製做 docSet 文檔

相關文章
相關標籤/搜索