PTT鄉民百科
(修正空白被吃掉的問題)
(support hr line and status bar)
第32行: 第32行:
   
 
class Parser {
 
class Parser {
processArticleId(text) {
+
processRegexp(text, regexp, command) {
 
let leftText = text
 
let leftText = text
const node = document.createDocumentFragment()
 
const aidRegexp = /#([-0-9A-Za-z_]{8})\b(?: \(([-\w]+)\))?/
 
 
let scan
 
let scan
while (scan = leftText.match(aidRegexp)) {
+
const result = []
  +
while (scan = leftText.match(regexp)) {
 
const beforeMatch = leftText.slice(0, scan.index)
 
const beforeMatch = leftText.slice(0, scan.index)
node.appendChild(
+
result.push(beforeMatch)
document.createTextNode(beforeMatch)
+
result.push(command(Array.from(scan)))
)
 
 
const aid = scan[1]
 
const board = scan[2] || 'ALLPOST'
 
const aidNode = this.aidToNode(aid, board)
 
node.appendChild(aidNode)
 
   
 
leftText = leftText.slice(scan.index + scan[0].length)
 
leftText = leftText.slice(scan.index + scan[0].length)
 
}
 
}
node.appendChild(
+
result.push(leftText)
 
return result
document.createTextNode(leftText)
 
)
+
}
  +
processAid(text) {
return node
 
  +
return this.processRegexp(text, this.aidRegexp, this.aidToNode)
  +
}
  +
get aidRegexp() {
 
return /#([-0-9A-Za-z_]{8})\b(?: \(([-\w]+)\))?/
 
}
 
}
aidToNode(aid, board) {
+
aidToNode(scan) {
 
const aid = scan[1]
 
const board = scan[2] || 'not-found'
 
const anchor = document.createElement('a')
 
const anchor = document.createElement('a')
 
const fileName = pttArticleId.toFileName(aid)
 
const fileName = pttArticleId.toFileName(aid)
第65行: 第64行:
 
token(text, type) {
 
token(text, type) {
 
const node = document.createElement('span')
 
const node = document.createElement('span')
 
node.textContent = text
if (type == 'comment') node.appendChild(this.processArticleId(text))
 
else node.textContent = text
 
 
node.classList.add(type)
 
node.classList.add(type)
 
return node
 
return node
第76行: 第74行:
 
for (const token of line) {
 
for (const token of line) {
 
if (typeof token == 'string') {
 
if (typeof token == 'string') {
if (typeof type == 'undefined' || type == 'quote') {
+
const textNode = document.createTextNode(token)
lineNode.appendChild(this.processArticleId(token))
+
lineNode.appendChild(textNode)
}
 
else {
 
const textNode = document.createTextNode(token)
 
lineNode.appendChild(textNode)
 
}
 
 
}
 
}
 
else if (token instanceof Node) {
 
else if (token instanceof Node) {
第110行: 第103行:
 
function command(match) {
 
function command(match) {
 
match[1] = this.token(match[1], 'head')
 
match[1] = this.token(match[1], 'head')
match[7] = this.token(match[3], 'head')
+
match[3] = this.token(match[3], 'head')
 
 
return this.line(match, 'head')
 
return this.line(match, 'head')
 
}
 
}
第125行: 第117行:
 
// → illumi:要去 op 板啊^^ 推 140.123.225.127 12/19
 
// → illumi:要去 op 板啊^^ 推 140.123.225.127 12/19
 
// → TomRiddle: 五樓厲害了 61.216.122.103 06/21 18:18
 
// → TomRiddle: 五樓厲害了 61.216.122.103 06/21 18:18
/^\s*([推噓→])( +)(\w[\w\d]*)(:.*?)((?:[推噓→] *)?(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\ )?\d{2}\/\d{2}(?: \d{2}:\d{2})?)?( *)$/,
+
/^\s*([推噓→])( +)(\w[\w\d]*)(:.*?)((?:[推噓→] *)?(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\ )?\d{2}\/\d{2}(?: \d{2}:\d{2})?)?( *)$/,
 
function command(match) {
 
function command(match) {
 
switch (match[1]) {
 
switch (match[1]) {
第139行: 第131行:
 
}
 
}
 
match[3] = this.token(match[3], 'username')
 
match[3] = this.token(match[3], 'username')
match[4] = this.token(match[4], 'comment')
+
match.splice(4, 1, ...this.processAid(match[4]))
 
if (match[5]) match[5] = this.token(match[5], 'date')
 
if (match[5]) match[5] = this.token(match[5], 'date')
 
return this.line(match, 'comment')
 
return this.line(match, 'comment')
 
}
  +
),
  +
{
  +
__proto__: Parser.prototype,
  +
// 瀏覽 第 1/7 頁 ( 11%) 目前顯示: 第 01~22 行 (y)回應(X%)推文(h)說明(← )離開
  +
regexp: /^(\s*瀏覽.*\d+%\)\s)(\s*目前顯示:.*行\s*)(.*)$/,
  +
command(match) {
  +
match[1] = this.token(match[1], 'percent')
  +
match[2] = this.token(match[2], 'line-number')
  +
match.splice(3,1,
  +
...this.processRegexp(
  +
match[3], this.keyRegexp, this.keyCommand.bind(this)
 
)
 
)
  +
return this.line(match, 'status')
  +
},
  +
keyRegexp: /\([^()]+\)/,
  +
keyCommand(scan) {
  +
return this.token(scan[0], 'key')
 
}
  +
},
  +
Parser.rule(
  +
/^─+$/,
  +
function (match) {
  +
match.push(match[0])
  +
return this.line(match, 'hr')
 
}
 
}
 
),
 
),
第147行: 第165行:
 
/^※.*$/,
 
/^※.*$/,
 
function command(match) {
 
function command(match) {
match.push(match[0])
+
const parseAid = this.processAid(match[0])
  +
match.push(...parseAid)
 
return this.line(match, 'system')
 
return this.line(match, 'system')
 
}
 
}
 
),
 
),
 
Parser.rule(
 
Parser.rule(
/^[:>](|\s.*)$/,
+
/^[:>](?:|\s.*)$/,
 
function command(match) {
 
function command(match) {
match[1] = match[0]
+
const parseAid = this.processAid(match[0])
  +
match.push(...parseAid)
 
return this.line(match, 'quote')
 
return this.line(match, 'quote')
 
}
 
}
第161行: 第181行:
 
/^.*$/,
 
/^.*$/,
 
function command(match) {
 
function command(match) {
match.push(match[0])
+
const parseAid = this.processAid(match[0])
  +
match.push(...parseAid)
 
return this.line(match)
 
return this.line(match)
 
}
 
}
第194行: 第215行:
 
}
 
}
 
}
 
}
pttHighlightArticle.processTemplate()
 
 
if (typeof exports == 'object') {
 
if (typeof exports == 'object') {
 
exports.pttHighlightArticle = pttHighlightArticle
 
exports.pttHighlightArticle = pttHighlightArticle
第204行: 第224行:
 
this.pttHighlightArticle = pttHighlightArticle
 
this.pttHighlightArticle = pttHighlightArticle
 
}
 
}
  +
 
pttHighlightArticle.processTemplate()
 
}()
 
}()

於 2019年6月22日 (六) 07:00 的修訂

// origin es+ version of [[MediaWiki:PttHighlightArticle.js]]
void function () {

    const pttArticleId = {
        decodeB64(b64) {
            let bitString = ''
            for (let i=0; i<b64.length; i++) {
                const charCode = b64.codePointAt(i)
                let bcode
                if (charCode == 45) bcode = 62 // -
                else if (charCode == 95) bcode = 63 // _
                else if (charCode <= 57) bcode = charCode - 48 // 0-9
                else if (charCode <= 90) bcode = charCode - 65 + 10 // A-Z
                else if (charCode <= 122) bcode = charCode - 97 + 36 // a-z
                else throw new Error('not ptt b64 string: ' + b64)
                bitString += this.formatPadBit(bcode)
            }
            return bitString
        },
        formatPadBit(value) {
            let string = value.toString(2)
            while (string.length < 6) string = '0' + string
            return string
        },
        toFileName(aid) {
            const bitString = this.decodeB64(aid)
            const time = parseInt(bitString.slice(0, -12), 2)
            const random = parseInt(bitString.slice(-12), 2)
            return `M.${time.toString(10)}.A.${random.toString(16)}`.toUpperCase()
        }
    }

class Parser {
    processRegexp(text, regexp, command) {
        let leftText = text
        let scan
        const result = []
        while (scan = leftText.match(regexp)) {
            const beforeMatch = leftText.slice(0, scan.index)
            result.push(beforeMatch)
            result.push(command(Array.from(scan)))

            leftText = leftText.slice(scan.index + scan[0].length)
        }
        result.push(leftText)
        return result
    }
    processAid(text) {
        return this.processRegexp(text, this.aidRegexp, this.aidToNode)
    }
    get aidRegexp() {
        return /#([-0-9A-Za-z_]{8})\b(?: \(([-\w]+)\))?/
    }
    aidToNode(scan) {
        const aid = scan[1]
        const board = scan[2] || 'not-found'
        const anchor = document.createElement('a')
        const fileName = pttArticleId.toFileName(aid)
        anchor.href = `https://www.ptt.cc/bbs/${board}/${fileName}.html`
        anchor.textContent = `#${aid} (${board})`
        anchor.classList.add('ptt-aid')
        return anchor
    }
    token(text, type) {
        const node = document.createElement('span')
        node.textContent = text
        node.classList.add(type)
        return node
    }
    line(line, type) {
        const lineNode = document.createElement('div')
        lineNode.classList.add('line')
        line.shift() // match()[0] is useless
        for (const token of line) {
            if (typeof token == 'string') {
                const textNode = document.createTextNode(token)
                lineNode.appendChild(textNode)
            }
            else if (token instanceof Node) {
                lineNode.appendChild(token)
            }
            else {
                console.error('unknown token: ', token)
            }
        }
        if (type) lineNode.classList.add(type)
        return lineNode
    }
    static rule(regexp, command) {
        const copy = new this()
        copy.regexp = regexp
        copy.command = command
        return copy
    }
}

const pttHighlightArticle = {
    Parser, pttArticleId,
    matchCommand: [
        Parser.rule(
                // /^(\s*作者 )( *)(\w[\w\d]*)( *\()(.*)(\) *)( 看板 ?)( ?)([-\d\w_]+)( *)$/,
                /^(\s*作者 )(.*)( 看板 )(.*)$/,
            function command(match) {
                match[1] = this.token(match[1], 'head')
                match[3] = this.token(match[3], 'head')
                return this.line(match, 'head')
            }
        ),
        Parser.rule(
                /^(\s*(?:標題|時間|轉信|作者|看板) )(.*)$/,
            function command(match) {
                match[1] = this.token(match[1], 'head')
                return this.line(match, 'head')
            }
        ),
        Parser.rule(
            // → illumi:要去 op 板啊^^                                 推 140.123.225.127 12/19
            // → TomRiddle: 五樓厲害了                            61.216.122.103 06/21 18:18
            /^\s*([推噓→])( +)(\w[\w\d]*)(:.*?)((?:[推噓→] *)?(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\ )?\d{2}\/\d{2}(?: \d{2}:\d{2})?)?( *)$/,
            function command(match) {
                switch (match[1]) {
                case '推':
                    match[1] = this.token(match[1], 'push')
                    break
                case '噓':
                    match[1] = this.token(match[1], 'boo')
                    break
                case '→':
                    match[1] = this.token(match[1], 'arrow')
                    break
                }
                match[3] = this.token(match[3], 'username')
                match.splice(4, 1, ...this.processAid(match[4]))
                if (match[5]) match[5] = this.token(match[5], 'date')
                return this.line(match, 'comment')
            }
        ),
        {
            __proto__: Parser.prototype,
            // 瀏覽 第 1/7 頁 ( 11%)  目前顯示: 第 01~22 行  (y)回應(X%)推文(h)說明(← )離開
            regexp: /^(\s*瀏覽.*\d+%\)\s)(\s*目前顯示:.*行\s*)(.*)$/,
            command(match) {
                match[1] = this.token(match[1], 'percent')
                match[2] = this.token(match[2], 'line-number')
                match.splice(3,1,
                    ...this.processRegexp(
                        match[3], this.keyRegexp, this.keyCommand.bind(this)
                    )
                )
                return this.line(match, 'status')
            },
            keyRegexp: /\([^()]+\)/,
            keyCommand(scan) {
                return this.token(scan[0], 'key')
            }
        },
        Parser.rule(
                /^─+$/,
            function (match) {
                match.push(match[0])
                return this.line(match, 'hr')
            }
        ),
        Parser.rule(
                /^※.*$/,
            function command(match) {
                const parseAid = this.processAid(match[0])
                match.push(...parseAid)
                return this.line(match, 'system')
            }
        ),
        Parser.rule(
                /^[:>](?:|\s.*)$/,
            function command(match) {
                const parseAid = this.processAid(match[0])
                match.push(...parseAid)
                return this.line(match, 'quote')
            }
        ),
        Parser.rule(
                /^.*$/,
            function command(match) {
                const parseAid = this.processAid(match[0])
                match.push(...parseAid)
                return this.line(match)
            }
        )
    ],

    textToNode(text) {
        const node = document.createElement('pre')
        for (let line of text.split(/\n/g)) {
            if (line.slice(-1) == '\r') line = line.slice(0,-1)
            for (const match of this.matchCommand) {
                const scan = line.match(match.regexp)
                if (scan) {
                    const lineNode = match.command(Array.from(scan))
                    node.appendChild(lineNode)
                    break
                }
            }
        }
        return node
    },

    processTemplate(root = document) {
        const rawList = root.querySelectorAll('.ptt-article.raw')
        for (const raw of rawList) {
            const node = this.textToNode(raw.textContent)
            node.className = raw.className
            node.classList.remove('raw')
            raw.parentNode.classList.remove('raw')
            raw.replaceWith(node)
        }
    }
}
    if (typeof exports == 'object') {
        exports.pttHighlightArticle = pttHighlightArticle
    }
    else if (typeof window == 'object') {
        window.pttHighlightArticle = pttHighlightArticle
    }
    else if (typeof this == 'object') {
        this.pttHighlightArticle = pttHighlightArticle
    }

    pttHighlightArticle.processTemplate()
}()