Bilibili-Markdown

B站专栏 Markdown 编辑器

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name                    Bilibili-Markdown
// @namespace               https://github.com/LuckyPuppy514
// @version                 1.0.4
// @author                  LuckyPuppy514
// @copyright               2023, Grant LuckyPuppy514 (https://github.com/LuckyPuppy514)
// @license                 MIT
// @description             B站专栏 Markdown 编辑器
// @homepage                https://github.com/LuckyPuppy514/Bilibili-Markdown
// @icon                    https://article.biliimg.com/bfs/article/3e927f211d063b57cd39c4041ac2d07fd959726c.png
// @match                   https://member.bilibili.com/article-text/home*
// @connect                 github.com
// @connect                 raw.githubusercontent.com
// @require                 https://unpkg.com/[email protected]/dist/jquery.min.js
// @grant                   GM.xmlHttpRequest
// ==/UserScript==

"use strict";

console.log(`

🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️
Ⓜ️                                                       🅱️
🅱️                   Bilibili-Markdown                   Ⓜ️
Ⓜ️                                                       🅱️
🅱️  https://github.com/LuckyPuppy514/Bilibili-Markdown   Ⓜ️
Ⓜ️                                                       🅱️
🅱️                  2023 @LuckyPuppy514                  Ⓜ️
Ⓜ️                                                       🅱️
🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️🅱️Ⓜ️

`);

// markdown 编辑器地址
// const BILIBILI_MARKDOWN_URL = "http://127.0.0.1:5500/web/tampermonkey/Bilibili-Markdown/index.html";
const BILIBILI_MARKDOWN_URL = "https://www.lckp.top/bilibili-markdown/index.html";
// id / name 公共前缀
const PREFIX = "bilibili-markdown-";
// 等待时间(ms)
const waitTime = {
    long: 2500,
    normal: 1000,
    short: 600,
};
// localStorage key
const key = {
    isMarkdown: "isMarkdown",
    isFullscreen: "isFullscreen"
}
// element id
const eid = {
    button: {
        switchToHtmlEditor: `${PREFIX}switch-to-html-editor-button`
    },
    iframe: {
        main: `${PREFIX}main-iframe`
    }
};
// element
const elements = {
    // 附加
    switchToMarkdownEditorButton: undefined,
    mainIframe: undefined,
    // 原有
    editorBox: undefined,
    loading: undefined,
    save: undefined,
    mbpreview: undefined
};
// class name
const cname = {
    fullscreen: `${PREFIX}fullscreen`,
    toast: `${PREFIX}toast`,
};
// z-index
const zIndex = {
    first: 999999,
    second: 999998
};
// display
const display = {
    none: "none",
    block: "block"
}

var needReload;
var bilibili;
var bilibiliMarkdown;

const CSS = `
/*切换 markdown 编辑器按钮*/
#${eid.button.switchToHtmlEditor} {
    font-size: 22px;
    border-width: 0px 1px 0px 0px;
    border-style: solid;
    border-color: white;
    margin-left: -9px;
    padding-right: 5px;
}
/*markdown 编辑器 iframe*/
#${eid.iframe.main} {    
    width: 100%;
    height: 480px;
    z-index: ${zIndex.second};
    border: none;
    display: none;
}
/*全屏*/
.${cname.fullscreen} {
    position: fixed !important;
    top: 0 !important;
    left: 0 !important;
    bottom: 0 !important;
    right: 0 !important;
    width: 100% !important;
    height: 100% !important;
    border: none !important;
    margin: 0 !important;
    padding: 0 !important;
    overflow: hidden !important;
    z-index: ${zIndex.second} !important;
}
/*消息*/
.${cname.toast} {
    max-width: 60%;
    min-width: 160px;
    padding: 0 14px;
    height: 50px;
    color: rgb(255, 255, 255);
    line-height: 50px;
    text-align: center;
    border-radius: 4px;
    position: fixed;
    top: 6%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: ${zIndex.first};
    background: rgba(119, 199, 104, 0.9);
    font-size: 14px;
    box-shadow: 0px 0px 10px rgba(119, 199, 104, 0.9);
}
/*手机端预览*/
.preview-mask,
.preview-mask .preview-content {
    padding-top: 35px !important;
    z-index: ${zIndex.first} !important;
}
`;
const HTML = `
<iframe id="${eid.iframe.main}" src="${BILIBILI_MARKDOWN_URL}"></iframe>
`;

function appendCSS() {
    let css = document.createElement("style");
    css.innerHTML = CSS.trim();
    document.head.appendChild(css);
}

function appendHTML() {
    let div = document.createElement("div");
    div.innerHTML = HTML.trim();
    document.getElementsByClassName("editor-wrap")[0].appendChild(div);
}

function appendSwitchToMarkdownEditorButton() {
    let button = document.createElement('li');
    button.id = eid.button.switchToHtmlEditor;
    button.className = 'toolbar-item left';
    button.innerHTML = 'Ⓜ️';
    document.getElementsByClassName('editor-toolbar clearfix')[0].prepend(button);
}

function getAllElement() {
    elements.switchToMarkdownEditorButton = document.getElementById(eid.button.switchToHtmlEditor);
    elements.mainIframe = document.getElementById(eid.iframe.main);

    elements.editorBox = document.getElementsByClassName("editor-box")[0];
    elements.save = document.getElementsByClassName("ui-btn white")[0];
    elements.mbpreview = document.getElementsByClassName("ui-btn white")[1];
    elements.loading = document.getElementById("loading");
    elements.loading.innerHTML = elements.loading.innerHTML.replace("玩儿命加载中", "处理中,请稍后");
    elements.loading.style.zIndex = zIndex.first;
}

function addListener() {
    elements.switchToMarkdownEditorButton.onclick = async function () {
        if (!bilibili.aid) {
            bilibili.aid = await bilibili.getAidFromLocalStorage();
        }
        if (bilibili.aid) {
            bilibili.switchToMarkdownEditor();
        } else {
            Toast("矮油,起码写个标题嘛~");
        }
    }
    elements.save.onclick = function () {
        if (localStorage.getItem(key.isMarkdown)) {
            bilibili.loading();
            setTimeout(() => {
                bilibiliMarkdown.save();
            }, waitTime.normal);
        }
    }
    elements.mbpreview.onclick = function () {
        if (localStorage.getItem(key.isMarkdown) && needReload) {
            setTimeout(() => {
                bilibiliMarkdown.save();
                bilibili.mbpreview();
            }, waitTime.normal);
        }
    }
}
// 显示消息
function Toast(msg, duration) {
    duration = isNaN(duration) ? 2000 : duration;
    let div = document.createElement("div");
    div.innerHTML = msg;
    div.className = cname.toast;
    document.body.appendChild(div);
    setTimeout(function () {
        div.style.opacity = "0";
        setTimeout(function () {
            document.body.removeChild(div)
        }, 500);
    }, duration);
}
// webp 转 jpg
function webpToJpg(webp) {
    return new Promise(function (resolve, reject) {
        let image = new Image();
        image.src = URL.createObjectURL(webp);
        image.onload = function () {
            let canvas = document.createElement("canvas");
            canvas.width = image.width;
            canvas.height = image.height;
            canvas.getContext("2d").drawImage(image, 0, 0);
            let blob = dataURLtoBlob(canvas.toDataURL("image/jpeg"));
            file = new File([blob], blob.name, {
                type: blob.type,
            });
            resolve(file);
        }
    });
}

function dataURLtoBlob(dataurl) {
    var arr = dataurl.split(','),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);
    while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8arr], {
        type: mime
    });
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

class Bilibili {
    constructor() {
        this.api = {
            upcover: "https://api.bilibili.com/x/article/creative/article/upcover",
            addupdate: "https://api.bilibili.com/x/article/creative/draft/addupdate",
        }
        this.page = {
            edit: "https://member.bilibili.com/platform/upload/text/edit",
            pcpreview: "https://www.bilibili.com/read/pcpreview",
            home: "https://member.bilibili.com/article-text/home"
        }
        this.csrf = this.getCsrf();
        this.aid = this.getAidFromLocation();
        this.addListener();
        this.uploading = 0;
        this.uploadList = new Map();
    }
    getCsrf() {
        let cookie = document.cookie;
        let csrf = cookie.substring(cookie.indexOf("bili_jct"));
        csrf = csrf.substring(9, csrf.indexOf(";"));
        return csrf;
    }
    getAidFromLocation() {
        let aid = undefined;
        let aids = window.location.href.match(/aid=[0-9]+/g);
        if (aids && aids.length > 0) {
            aid = aids[0].replace("aid=", "");
        }
        if (aid && aid.toString().length > 5) {
            return aid;
        }
        return undefined;
    }
    async getAidFromLocalStorage() {
        let aid = undefined;
        // 等待 TIMEOUT_TIME 后读取 localStorage (更新需要时间)
        this.loading();
        await new Promise((resolve, reject) => {
            setTimeout(() => {
                aid = JSON.parse(localStorage.bili_localDraft).id;
                resolve();
            }, waitTime.long);
        })
        this.hideLoading();

        if (aid && aid.toString().length > 5) {
            // 新建专栏跳转编辑页面
            if (window.location.href.endsWith("?")) {
                top.location.href = this.page.edit + "?aid=" + aid;
            }
            return aid;
        }
        return undefined;
    }
    addListener() {
        window.addEventListener("message", function (event) {
            bilibili[event.data.method](event.data.param);
        }, false);
    }
    switchToMarkdownEditor() {
        localStorage.setItem(key.isMarkdown, true);
        elements.mainIframe.style.display = display.block;
        elements.editorBox.style.display = display.none;
        if (localStorage.getItem(key.isFullscreen)) {
            localStorage.removeItem(key.isFullscreen);
            this.switchToFullscreen();
        }
    }
    switchToHtmlEditor() {
        localStorage.removeItem(key.isMarkdown);
        elements.mainIframe.style.display = display.none;
        elements.editorBox.style.display = display.block;
        document.body.style.overflowY = "";
        if (needReload) {
            needReload = false;
            location.reload();
        }
    }
    switchToFullscreen(param) {
        if (param && param.isFullscreen != undefined) {
            if (param.isFullscreen === true) {
                fullscreen();
                if (top != self) {
                    top.location.href = bilibili.page.home + "?aid=" + bilibili.aid;
                }
            } else {
                exitFullscreen();
            }
        } else {
            if (localStorage.getItem(key.isFullscreen)) {
                exitFullscreen();
            } else {
                fullscreen();
            }
        }

        function fullscreen() {
            localStorage.setItem(key.isFullscreen, true);
            elements.mainIframe.className = cname.fullscreen;
            document.body.style.overflowY = "hidden";
        }

        function exitFullscreen() {
            localStorage.removeItem(key.isFullscreen);
            elements.mainIframe.className = "";
            document.body.style.overflowY = "";
        }
    }
    loading() {
        elements.loading.style.display = display.block;
        setTimeout(this.hideLoading, waitTime.long);
    }
    hideLoading() {
        elements.loading.style.display = display.none;
    }
    pcpreview() {
        window.open(this.page.pcpreview + "?aid=" + this.aid);
    }
    mbpreview() {
        if (needReload) {
            localStorage.setItem(key.needMbpreview, true);
            location.reload();
        } else {
            localStorage.removeItem(key.needMbpreview);
            document.getElementsByClassName("ui-btn white")[1].click();
        }
    }
    async appendImage(param) {
        bilibiliMarkdown.appendImage(await this.uploadImage(param.image));
    }
    toBLink(param) {
        this.loading();
        let link = param.link;
        GM.xmlHttpRequest({
            method: "GET",
            url: link,
            responseType: "blob",
            onload: async function (response) {
                let image = new File([response.response], link.substring(link.lastIndexOf('/') + 1));
                let bLink = await bilibili.uploadImage(image);
                bilibiliMarkdown.toBLink(link, bLink);
                bilibili.hideLoading();
            },
            onerror: function (error) {
                console.error("请求失败:", error);
                bilibili.hideLoading();
            }
        });
    }
    async uploadImage(image) {
        let name = image.name;
        let bLink = this.uploadList.get(name);
        if (bLink) {
            if (bLink == "uploading") {
                return undefined;
            } else {
                return bLink;
            }
        } else {
            this.uploadList.set(name, "uploading");
        }
        // webp 转 jpg
        if (name.endsWith(".webp")) {
            image = await webpToJpg(image);
        }
        bLink = "图片上传B站失败,请重试";
        let formData = new FormData();
        formData.append("binary", image);
        formData.append("csrf", this.csrf);

        // 限制上传频率
        let that = this;
        while (that.uploading > 0) {
            await sleep(waitTime.normal);
        }
        that.uploading++;
        $.ajax({
            type: "POST",
            contentType: false,
            processData: false,
            async: false,
            data: formData,
            url: bilibili.api.upcover,
            xhrFields: {
                withCredentials: true
            },
            success: function (res) {
                if (res && res.data) {
                    bLink = res.data.url;
                    that.uploadList.set(name, bLink);
                } else {
                    that.uploadList.delete(name);
                    Toast("上传失败:" + JSON.stringify(res));
                }
            }
        })

        // 释放限制频率锁
        setTimeout(() => {
            that.uploading--;
        }, waitTime.normal);
        return bLink;
    }
    async tableToImage(html, tables) {
        if (tables && tables.size > 0) {
            for (let [oldHtml, image] of tables) {
                let bLink = await this.uploadImage(image);
                let newHtml = `<figure contenteditable="false" class="img-box"><img referrerpolicy="no-referrer" src="${bLink}"><figcaption class="caption" contenteditable="false"></figcaption></figure>`;
                html = html.replaceAll(oldHtml, newHtml);
            }
        }
        return html;
    }
    async save(param) {
        let html = param.html ? param.html : "";
        // 保存到本地
        localStorage.setItem(PREFIX + this.aid, param.markdown);
        // 表格转图片
        html = await this.tableToImage(html, param.tables);
        // 提取内容
        let words = html.replace(/<(h[1-6]|code)[^>]*>[^<]*<\/\1>/g, "")
            .replace(/<[^>]*>/g, "")
            .replace(/[\s| |\n\|\r]*/g, "");
        // 提取总结
        let summary = words.slice(0, 100);
        // B站接口参数
        let biliLocalDraft = JSON.parse(localStorage.bili_localDraft);
        $.ajax({
            type: "POST",
            data: {
                title: biliLocalDraft.title,
                content: html,
                summary: summary,
                words: words.length,
                category: biliLocalDraft.category,
                list_id: biliLocalDraft.list_id,
                tid: biliLocalDraft.template.id,
                reprint: 0,
                media_id: biliLocalDraft.media_id,
                spoiler: biliLocalDraft.is_spoiler ? "1" : "0",
                original: biliLocalDraft.isOriginal,
                aid: biliLocalDraft.id,
                csrf: this.csrf
            },
            url: bilibili.api.addupdate,
            xhrFields: {
                withCredentials: true
            },
            success: function (res) {
                bilibili.hideLoading();
                if (res && res.code == 0) {
                    if (localStorage.getItem(key.needMbpreview)) {
                        location.reload();
                    } else {
                        needReload = true;
                        Toast(" 草稿已保存 ");
                    }
                } else {
                    Toast("保存失败: " + JSON.stringify(res));
                }
            },
            error: function (err) {
                Toast("保存失败: " + JSON.stringify(err.message));
            }
        });
    }
}

class BilibiliMarkdown {
    constructor() {
        setTimeout(() => {
            this.hello();

            if (bilibili.aid) {
                if (localStorage.getItem(key.isMarkdown)) {
                    bilibili.switchToMarkdownEditor();
                }

                let markdown = localStorage.getItem(PREFIX + bilibili.aid);
                if (markdown) {
                    this.setMarkdown(markdown);
                }
                if (localStorage.getItem(key.needMbpreview)) {
                    bilibili.mbpreview();
                }
            }
        }, waitTime.short);
    }
    message(method, param) {
        elements.mainIframe.contentWindow.postMessage({
            method: method,
            param: param
        }, BILIBILI_MARKDOWN_URL);
    }
    hello() {
        this.message(this.hello.name);
    }
    save() {
        this.message(this.save.name);
    }
    toBLink(link, bLink) {
        if (bLink) {
            this.message(this.toBLink.name, {
                link: link,
                bLink: bLink
            });
        }
    }
    appendImage(bLink) {
        this.message(this.appendImage.name, {
            bLink: bLink
        });
    }
    setMarkdown(markdown) {
        this.message(this.setMarkdown.name, {
            markdown: markdown
        });
    }
}

window.onload = function () {
    setTimeout(() => {
        let saveButton = document.getElementsByClassName("ui-btn white")[0];
        if (!saveButton || saveButton.innerHTML != "存草稿") {
            console.log("文章已提交");
            return;
        }

        appendCSS();
        appendHTML();
        appendSwitchToMarkdownEditorButton();
        getAllElement();
        addListener();

        bilibili = new Bilibili();
        bilibiliMarkdown = new BilibiliMarkdown();
    }, waitTime.short);
}