Bilibili-Markdown

B站专栏 Markdown 编辑器

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);
}