build1
code:script.js
// いま表示しているページ
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 = () => {
return PIMENT_ORIGIN + PIMENT_PATHNAME
}
window.setPimentoOrigin = (origin, pathname = '') => {
try {
PIMENT_ORIGIN = (new URL(origin)).origin
if (pathname && pathname.startsWith('/')) {
PIMENT_PATHNAME = pathname.trim()
}
} catch (err) {
throw err
}
}
const sendMessage = (win, body) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
win.postMessage(body, getPimentAppUrl())
resolve()
}, 1500) // 50 // 100
})
}
const createPimentoWindow = async data => {
data.projectName = (scrapbox.Project.name || location.pathname.split('/')1) // 古いウィンドウを閉じる
if (pimentoWindow && !pimentoWindow.closed) {
pimentoWindow.postMessage({ task: 'close' }, getPimentAppUrl())
}
if (!PIMENT_ORIGIN) return alert('PIMENT_ORIGIN is required!')
pimentoWindow = window.open(getPimentAppUrl())
window.requestAnimationFrame(async () => {
for (let i = 0; i < 6; i++) {
if (pimentoWindow.closed) return
await sendMessage(pimentoWindow, data)
}
})
}
const previewThisPage = async () => {
const body = await createPageData()
const refs = []
for (const pageId of Object.keys(refPages)) {
if (body.id === pageId) continue
}
await createPimentoWindow({
task: 'transfer-data',
type: 'page',
body,
icons,
template: await parseTemplate(),
refs
})
}
const previewWholePages = async () => {
// 探索開始ページ(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)
}
//console.clear()
const toc = await createToc()
const body, icons = await fetchBookPages(Array.from(entryPageLinks)) await createPimentoWindow({
task: 'transfer-data',
type: 'whole-pages',
refresh: true,
body,
icons,
bookTitle: scrapbox.Page.title.trim(),
toc,
template: await parseTemplate(toc.preamble)
})
}
scrapbox.PageMenu.addMenu({
title: '製本',
onClick: async () => {
const tagName = encodeURIComponent("pimento目次")
const tag = document.querySelector(a.page-link[href$="${tagName}"])
console.clear()
if (tag) {
// 目次ページでの起動
await previewWholePages()
} else {
await previewThisPage()
}
}
})
// 埋め込み指定された節は解決しないモード
scrapbox.PageMenu.addMenu({
title: 'このページをプレビュー',
onClick: async () => {
const body = await createPageData()
const icons = Object.create(null) // { title: gyazoUrl }
await resolveIconGyazoId(icons, { icons: body.icons })
await createPimentoWindow({
task: 'transfer-data',
type: 'page',
refresh: true,
body,
icons,
template: await parseTemplate()
})
}
})