script.jsconst loadTitles = ({useLinks}) => {
fetch(`/api/pages/${scrapbox.Project.name}/search/titles`, {
credentials: 'same-origin'
}).then(res => {
return res.json()
}).then(data => {
let titles = data.map(p => p.title)
if (useLinks) {
data.map(p => {
p.links.map(l => {if (!titles.includes(l)) titles.push(l)})
})
}
titles = titles.sort((a, b) => b.length - a.length)
window.titles = titles
console.log('omakase-links-✨: load titles, links')
scrapbox.PopupMenu.addButton({
title: '[✨]',
onClick: autoLink
})
})
}
// 既に記法になっているなどの理由で、置換すべきでない範囲を取得する
const detectLocked = text => {
const syntax = /\[*\[.+?\]\]*/g
const locked = text.split('').map(c => false)
let res
while (res = syntax.exec(text)) {
for (let i = 0; i < res[0].length; i++) locked[i + res.index] = true
}
return locked
}
const autoLink = text => {
if (!window.titles || !text) return text
const locked = detectLocked(text)
const matched = text.split('').map(c => null)
for (const title of window.titles) {
const regexp = new RegExp(escapeTitle(title), 'gi')
let res, found = false
while (!found && (res = regexp.exec(text))) {
const len = res[0].length
const idx = res.index
if (matched[idx] === null && !locked[idx]) {
found = true
const len = res[0].length
for (let i = 0; i < len; i++) {
if (i === 0 || i === len - 1) {
let c = text[idx + i]
if (i === 0) c = `[${c}`
if (i === len - 1) c = `${c}]`
matched[idx + i] = c
} else {
matched[idx + i] = text[idx + i]
}
}
}
}
}
// 無駄にテロメアがupdateされるのを防ぐ為、何も置換しない時は何も返さない
if (matched.join('').length === 0) return
// 配列matchedでnullの位置を、元のtextの文字に置き換える
return matched.map((c, idx) => c === null ? text[idx] : c).join('')
}
const escapeTitle = title => {
return title.replace(/[$-\/?[-^{|}]/g, '\\$&')
}
// 置換候補をリアルタイムで更新できないのがツライところ
// しかし、毎回API呼んでると時間かかるので、ひとまず妥協
if (!window.titles) loadTitles({useLinks: true})