build v2.0.0
code:script.js
const USERSCRIPT_VERSION = '2.0.0'
// いま表示しているページ
const createPageData = async () => {
const projectName = scrapbox.Project.name
const pageTitle = encodeURIComponent(scrapbox.Page.title)
const res = await fetch(/api/pages/${projectName}/${pageTitle})
const { id, title, lines, icons } = await res.json()
return { id, title, lines, icons }
}
const isEmptyLink = titleLc => {
for (const page of scrapbox.Project.pages) {
if (page.titleLc !== titleLc) continue
return !page.exists
}
return true
}
code:script.js
// テンプレートを取得して解釈する
const parseTemplate = async (preamble = []) => {
const projectTemplateUrl = /api/code/${scrapbox.Project.name}/_pimento/template.tex
let res = await fetch(projectTemplateUrl)
if (!res.ok) {
const defaultTemplateUrl = '/api/code/daiiz-pimento/_pimento/template.tex'
console.log('Use default template:', defaultTemplateUrl)
res = await fetch(defaultTemplateUrl)
if (!res.ok) {
throw new Error('template not found')
}
}
const codeLines = (await res.text()).split('\n')
const template = { headLines: [], tailLines: [] }
let isInHead = true
for (let codeLine of codeLines) {
const trimedCodeLine = codeLine.trim()
if (isInHead && trimedCodeLine === '% =====pimento-page-preamble=====') {
codeLine = preamble.join('\n')
}
if (trimedCodeLine === '% =====pimento-book-content=====') {
isInHead = false
}
if (!trimedCodeLine) continue
if (isInHead) {
template.headLines.push(codeLine)
} else {
template.tailLines.push(codeLine)
}
}
return template
}
code:script.js
const resolveIconGyazoId = async (dict, { icons }) => {
const projectName = scrapbox.Project.name
for (const icon of icons.map(title => toTitleLc(title))) {
const iconPageTitle = encodeURIComponent(icon)
const apiUrl = /api/pages/${projectName}/${iconPageTitle}
const res = await fetch(apiUrl, { method: 'GET' })
if (!res.ok) {
continue
}
const { image } = await res.json()
if (/^https:\/\/gyazo.com\//.test(image)) {
}
}
}
code:script.js
// 開始ページリストを受け取って有効なリンクを辿っていく
const toTitleLc = title => title.toLowerCase().replace(/\s/g, '_')
const COUNTER_LIMIT = 30
const fetchBookPages = async (entries = []) => {
const projectName = scrapbox.Project.name
if (entries.length === 0) return []
entries = entries.map(title => toTitleLc(title))
const pages = Object.create(null)
const pageIconGyazoUrls = Object.create(null) // { title: gyazoUrl }
const visitedPageLinks = new Set() // ClosedList
const pageLinks = [] // OpenList
let counter = 0
const breadthFirstSearch = async () => {
console.log(${counter}: OpenList:, pageLinks)
if (pageLinks.length === 0 || counter >= COUNTER_LIMIT) return
counter += 1
const titleLc = pageLinks.shift()
visitedPageLinks.add(titleLc)
const res = await fetch(/api/pages/${projectName}/${encodeURIComponent(titleLc)})
const { links, icons, id, title, lines, persistent } = await res.json()
if (persistent) {
pagesid = { title, lines: lines.map(line => line.text) } }
// アイコン記法で表示されるGyazoIdを解決
await resolveIconGyazoId(pageIconGyazoUrls, { icons })
// linksをOpenListに追加する
for (const link of links) {
const linkLc = toTitleLc(link)
// 仕様:「_」で始まるページは無視
if (linkLc.startsWith('_')) continue
if (linkLc.startsWith('ref=')) continue
// 外部プロジェクトは探索しない
// EmptyLinkである場合は探索リストに追加しない
if (isEmptyLink(linkLc)) continue
if (!pageLinks.includes(linkLc) && !visitedPageLinks.has(linkLc)) {
pageLinks.push(linkLc)
}
}
return breadthFirstSearch()
}
for (const entry of entries) {
console.log('entry:', entry)
pageLinks.push(entry)
await breadthFirstSearch()
}
if (pageLinks.length > 0) {
console.error('Exceeded COUNTER_LIMIT, OpenList:', pageLinks)
console.error('ClosedList:', visitedPageLinks)
}
}
code:script.js
// プリアンブル、前書き、後書きを取得する
const getBookText = async () => {
const textBlocks = Object.create(null)
for (const fileName of fileNames) {
const pageTitle = encodeURIComponent(scrapbox.Page.title)
const url = /api/code/${scrapbox.Project.name}/${pageTitle}/${fileName}.tex
const res = await fetch(url, { method: 'GET' })
if (!res.ok) continue
const text = await res.text()
}
return textBlocks
}
code:script.js
// 目次を構成
const createToc = async () => {
const parts = Object.create(null)
const flatChaps = []
let currentPartTitle = ''
const lines = document.querySelectorAll('.page .lines div.line')
for (const line of lines) {
const part = line.querySelector('strong.level-1 span.deco-\\*') // 部
if (part) {
currentPartTitle = part.innerText
if (currentPartTitle in parts) {
throw new Error('Parts are duplicated: ' + currentPartTitle)
}
}
const chaps = line.querySelectorAll('span.indent a.page-link')
for (const chap of chaps) {
const span = chap.closest('span.indent')
if (span.style.marginLeft !== '1.5em') continue
const chapTitle = chap.innerText
currentPartTitle
: flatChaps.push(chapTitle)
}
}
// 空の部を除去
const partTitles = Object.keys(parts)
for (const partTitle of partTitles) {
}
// 前書きと後書きを取得
const { preface, postscript, preamble } = await getBookText()
return { parts, flatChaps, preface, postscript, preamble }
}
code:script.js
let pimentoWindow = null
let PIMENT_ORIGIN = null
let PIMENT_PATHNAME = '/'
const getPimentAppUrl = (nextAction = '') => {
let baseUrl = PIMENT_ORIGIN + PIMENT_PATHNAME + ?uv=${USERSCRIPT_VERSION}
if (nextAction) {
baseUrl += &job=${nextAction}
}
return baseUrl
}
const getPimentoAppOrigin = () => {
if (!PIMENT_ORIGIN) {
throw new Error('PIMENT_ORIGIN is empty')
}
return PIMENT_ORIGIN
}
const isLocalToolsMode = () => {
}
window.setPimentoOrigin = (origin, pathname = '') => {
try {
PIMENT_ORIGIN = (new URL(origin)).origin
if (pathname && pathname.startsWith('/')) {
PIMENT_PATHNAME = pathname.trim()
}
} catch (err) {
throw err
}
}
const main = async (job) => {
switch (job) {
// 単一ページのプレビュー
case 'preview-single-page': {
const body = await createPageData()
const icons = Object.create(null) // { title: gyazoUrl }
await resolveIconGyazoId(icons, { icons: body.icons })
await requestToPimentoWindow({
task: 'transfer-data',
type: 'page',
refresh: true,
body,
icons,
template: await parseTemplate()
})
break
}
// 製本 (目次以外のページで起動されたとき)
case 'preview-page': {
const body = await createPageData()
const refs = []
for (const pageId of Object.keys(refPages)) {
if (body.id === pageId) continue
}
await requestToPimentoWindow({
task: 'transfer-data',
type: 'page',
body,
icons,
template: await parseTemplate(),
refs
})
break
}
// 製本 (目次ページで起動されたとき)
case 'whole-pages': {
// 探索開始ページ(indentLevel=1のpageLink)を抽出する
const candidates = document.querySelectorAll('span.indent a.page-link')
const entryPageLinks = new Set()
for (const candidate of candidates) {
const span = candidate.closest('span.indent')
if (span.style.marginLeft === '1.5em') entryPageLinks.add(candidate.innerText)
}
const toc = await createToc()
const body, icons = await fetchBookPages(Array.from(entryPageLinks)) await requestToPimentoWindow({
task: 'transfer-data',
type: 'whole-pages',
refresh: true,
body,
icons,
bookTitle: scrapbox.Page.title.trim(),
toc,
template: await parseTemplate(toc.preamble)
})
break
}
}
}
// createPimentoEmptyWindowで生成したウィンドウからのメッセージを受ける
window.addEventListener('message', async ({ origin, data }) => {
if (origin !== PIMENT_ORIGIN) return
const { pimentoReady, pimentoParams } = data
if (!pimentoReady) return
console.log('job:', pimentoParams.job) await main(pimentoParams.job)
})
const sendMessage = (win, body) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
win.postMessage(body, getPimentoAppOrigin()) // aaa
resolve()
}, 1000) // 50 // 100
})
}
const createPimentoEmptyWindow = (nextAction) => {
// 古いウィンドウを閉じる
if (pimentoWindow && !pimentoWindow.closed) {
pimentoWindow.postMessage({ task: 'close' }, getPimentoAppOrigin())
}
if (!PIMENT_ORIGIN) {
return alert('PIMENT_ORIGIN is required!')
}
pimentoWindow = window.open(getPimentAppUrl(nextAction))
if (isLocalToolsMode()) {
console.log('job local tools mode:', nextAction) main(nextAction)
}
}
// すでに開いているPimentoWindowに対してデータを送る
const requestToPimentoWindow = async data => {
if (!pimentoWindow) {
return alert('pimentoWindow is not ready!')
}
data.projectName = (scrapbox.Project.name || location.pathname.split('/')1) await sendMessage(pimentoWindow, data)
}
scrapbox.PageMenu.addMenu({
title: '製本',
onClick: async () => {
const tagName = encodeURIComponent("pimento目次")
const tag = document.querySelector(a.page-link[href$="${tagName}"])
console.clear()
if (tag) {
// 目次ページでの起動
await createPimentoEmptyWindow('whole-pages')
} else {
await createPimentoEmptyWindow('preview-page')
}
}
})
// 埋め込み指定された節は解決しないモード
scrapbox.PageMenu.addMenu({
title: 'このページをプレビュー',
onClick: async () => {
await createPimentoEmptyWindow('preview-single-page')
}
})