build
code:script.js
const USERSCRIPT_VERSION = '2.1.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 isPageLinkAsTextBlockRef = (linkLc, lines) => {
const normLinkLc = linkLc.replace(/_/g, ' ')
for (const line of lines) {
if (line.trim().length === 0) continue
const normLineLc = line.toLowerCase().replace(/\[\*+\s+\[/g, '[* [').replace(/_/g, ' ')
if (normLineLc.includes([* [${normLinkLc}]])) {
return true
}
}
return false
}
const fetchBookPages = async (entries = [], enableAppendix = false) => {
console.log('enableAppendix:', enableAppendix)
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
// Appendixが無効な場合は、通常のpageLinkは辿らなくてよい
if (!enableAppendix && !isPageLinkAsTextBlockRef(linkLc, pagesid.lines)) { console.log('NotBlockRef, skip:', 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 = () => {
}
const setPimentoOrigin = (origin, pathname = '') => {
try {
PIMENT_ORIGIN = (new URL(origin)).origin
if (pathname && pathname.startsWith('/')) {
PIMENT_PATHNAME = pathname.trim()
}
} catch (err) {
throw err
}
}
const isEnabledAppendix = template => {
const { tailLines } = template
if (!tailLines) return false
const lines = tailLines.map(line => line.replace(/^\s*%\s+/, '').replace(/\s.+$/, ''))
return lines.includes('appendix=true')
}
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 template = await parseTemplate()
const enableAppendix = isEnabledAppendix(template)
const refs = []
for (const pageId of Object.keys(refPages)) {
if (body.id === pageId) continue
}
await requestToPimentoWindow({
task: 'transfer-data',
type: 'page',
body,
icons,
template,
refs
})
break
}
// 製本 (目次ページで起動されたとき)
// case...
}
}
const sendMessage = (win, body) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
win.postMessage(body, getPimentoAppOrigin())
resolve()
}, 1000)
})
}
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)
}
// 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 initPimento = (customOrigin = "") => {
const pathname = "/new"
setPimentoOrigin(customOrigin || origin, pathname)
// scrapbox.PageMenu.addMenu({
// title: '製本',
// onClick: async () => {
// }
// })
// 埋め込み指定された節は解決しないモード
scrapbox.PageMenu.addMenu({
title: 'このページをプレビュー',
onClick: async () => {
await createPimentoEmptyWindow('preview-single-page')
}
})
console.log('Pimento version', USERSCRIPT_VERSION) }
export { initPimento }