// ==UserScript==
// @name Bilibili-Markdown
// @namespace https://github.com/LuckyPuppy514
// @version 1.0.3
// @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*
// @require https://unpkg.com/jquery@3.2.1/dist/jquery.min.js
// ==/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;
let xhr = new XMLHttpRequest();
xhr.open("get", link, true);
xhr.responseType = "blob";
xhr.onload = async function () {
let image = new File([xhr.response], link.substring(link.lastIndexOf('/') + 1));
let bLink = await bilibili.uploadImage(image);
bilibiliMarkdown.toBLink(link, bLink);
bilibili.hideLoading();
}
xhr.send();
}
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);
}