cosense-katex-completion
対象
たとえば$ \forall\varepsilon>0\exist\delta>0
code:ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
import { makeMeasure } from "./bitap.ts";
import {
editor,
textInput,
insertText,
press,
caret,
getCharDOM,
getInternalLink,
takeCursor,
takeSelection,
} from "jsr:@cosense/std@0.29/browser/dom";
import { loadEmojis } from "./load.ts";
import { makeBox } from "./ui.ts";
import { Candidate } from "./types.ts";
import { getMaxDistance } from "./distance.ts";
const projectName = scrapbox.Project.name;
const emojis: Candidate[] = await loadEmojis(projectName);
scrapbox.PageMenu.addMenu({
title: "emoji",
});
scrapbox.PageMenu("emoji").addItem({
title: "load emojis from /emoji",
onClick: async () => {
for (const emoji of await loadEmojis("emoji")) {
if(emojis.some((e) => emoji.name === e.name)) continue;
emojis.push(emoji);
}
},
});
const getFormulaRange = (line: number, char: number): { start: number; end: number; } | undefined => {
const formulaDOM = getCharDOM(line,char)?.closest?.("span.formula") as (HTMLSpanElement | undefined);
if (!formulaDOM) return;
(char) => parseInt(char.dataset.charIndex)
);
return {
start: Math.min(...indice),
end: Math.max(...indice),
};
};
const detectFormula = () => {
const { line, char } = caret().position;
const range = getFormulaRange(line, char);
const link = getInternalLink(line, char);
if (!link) return undefined;
const text = /^\^\+\]$/.test(link.textContent) ?
link.textContent.replace(/^\[(^\]+)\]$/, "$1").trim() : link.textContent.trim();
const start = getIndex(getChars(link).next().value);
return text.startsWith(":") ? {
text: text.slice(1),
raw: [${text}],
pos: {
line,
char: start,
} : undefined;
};
let completing = false;
const callback = () => {
const text = detectLink()?.text;
if (text !== undefined) return;
handleEnd();
};
const handleStart = () => {
if (completing) return;
completing = true;
const cursor = takeCursor();
cursor.addChangeListener(callback);
};
const handleEnd = () => {
if (!completing) return;
completing = false;
const cursor = takeCursor();
cursor.removeChangeListener(callback);
close();
};
textInput!.addEventListener("input", (e) => {
if (e.isComposing) return;
const text = detectLink()?.text;
if (text === undefined) {
handleEnd();
return;
}
const { match } = Asearch( ${text} );
const compare = new Intl.Collator().compare;
setItems(emojis
.flatMap((emoji) => {
const result = match(emoji.name, getMaxDistancetext.length); if (!result.found) return [];
return [{
distance: result.distance,
onClick: () => {
const link = detectLink();
if (link === undefined) {
handleEnd();
return;
}
const selection = takeSelection();
selection.setSelection({
start: {
line: link.pos.line,
char: link.pos.char,
},
end: {
line: link.pos.line,
char: link.pos.char + link.raw.length -1,
},
});
await insertText();
},
...emoji
}];
})
.sort((a, b) => a.distance === b.distance ? compare(a.name, b.name) : a.distance - b.distance)
);
open();
});
editor.keydown( e => {
const key = e.key;
const cursor = $('#text-input')0; stack += e.key;
let focused = $(':focus');
if(focused.is(items.find('li > a'))){
cursor.focus();
}
}
switch(key){
case 'Backspace':
stack = stack.slice(0, stack.length - 1);
if(stack.length === 0){
close();
return;
}
break;
case 'ArrowUp':
let focusedUp = $(':focus');
if( focusedUp.is(items.find('li > a').eq(0)) ){
e.stopPropagation();
cursor.focus();
}else if( !focusedUp.is(items.find('li > a')) ){
close();
return;
}
break;
case 'ArrowDown':
let focusedDown = $(':focus');
if( !focusedDown.is(items.find('li > a'))) {
e.stopPropagation();
e.preventDefault();
items.find("li > a").eq(0).focus();
}
break;
case 'Escape':
case 'ArrowLeft':
case 'ArrowRight':
case 'Home':
case 'End':
case 'PageUp':
case 'PageDown':
close();
break;
case 'Enter':
if( stack.length === 1 ){
close();
break;
}
let focused = $(':focus');
if(!focused.is(items.find('li > a'))){
e.stopPropagation();
e.preventDefault();
items.find('li > a').eq(0).click();
}
break;
}
if( stack.length <= 1 || !key.match(/^\w\s\:\-\+$|Backspace/)) return; const matchedEmoji = fizzSearch(stack, emojis)
if( matchedEmoji.length === 0){
close();
return;
}
const newItems = $('<ul>').addClass('dropdown-menu');
matchedEmoji.forEach( ( emoji, index) => {
if( index > 30 ) return;
newItems.append(makeItem(emoji.name, emoji.src));
a.on('click', () => {
cursor.focus();
replaceText(stack, cursor, emoji.path);
})
a.on('keypress', ev => {
if(ev.key === "Enter"){
ev.preventDefault();
ev.stopPropagation();
replaceText(stack, cursor, emoji.path);
}
})
})
items.replaceWith(newItems);
items = newItems;
let css = {};
cursor.style.cssText.split(';').filter( text => text !== '' )
.forEach( text => {
const props = text.split(':').map( text => text.replace(' ', '').replace('px', ''));
});
box.css({
top: ${parseInt(css.top) + parseInt(css.height) + 3}px,
left: ${css.left}px,
});
})
2025-02-28
18:33:49 意外と難航しそう
数式の記号と識別子とを分割しないといけない
識別子を全部列挙しなければならず、それにはKaTeXのlexerを調べないといけない 18:43:24 tokenRegexStringを使えばよさそう
18:49:49 そのままだと変だな
https://gyazo.com/bc7e6960c8796b43a72dd3715b23d144
いらない正規表現を削ろう
code:tokenize.ts
const spaceRegexString = " \r\n\t"; const controlWordRegexString = "\\\\a-zA-Z@+"; const controlWordWhitespaceRegexString =
(${controlWordRegexString})${spaceRegexString}*;
const controlSpaceRegexString = "\\\\(\n| \r\t+\n?) \r\t*"; export const tokenRegex = new RegExp((${spaceRegexString}+)| + // whitespace
${controlSpaceRegexString}| + // \whitespace
"([!-\\\\-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint ${combiningDiacriticalMarkString}* + // ...plus accents
${combiningDiacriticalMarkString}* + // ...plus accents
"|\\\\verb\\*(^).*?\\4" + // \verb* "|\\\\verb(^*a-zA-Z).*?\\5" + // \verb unstarred |${controlWordWhitespaceRegexString} + // \macroName + spaces
|${controlSymbolRegexString})); // \\, \', etc.
globalThis.tokenRegex = tokenRegex;
空文字""で検索したときdistanceがInfinityになってしまうバグがある
後ほど直す
code:bitap.ts
const INITPAT = 0x80000000;
/**
* Make a function that measures the Levenshtein distance between the source and the given text.
*
* @example
* `ts
* import { assertEquals } from "jsr:@std/assert@1/equals";
*
* assertEquals(makeMeasure("")(""), 0);
* assertEquals(makeMeasure("")("a"), 1);
* assertEquals(makeMeasure("")("ab"), 2);
* assertEquals(makeMeasure("")("abc"), 3);
* assertEquals(makeMeasure("")("abcd"), 4);
* assertEquals(makeMeasure("")("abcde"), 5);
* assertEquals(makeMeasure("a")("a"), 0);
* assertEquals(makeMeasure("a")(""), Infinity);
* assertEquals(makeMeasure("a")("b"), 1);
* assertEquals(makeMeasure("a")("ab"), 1);
* assertEquals(makeMeasure("a")("bab"), 2);
* assertEquals(makeMeasure("a")("abcd"), 3);
* assertEquals(makeMeasure("a")("bcde"), Infinity);
* assertEquals(makeMeasure("a")("abcde"), Infinity);
* assertEquals(makeMeasure("ab")("ab"), 0);
* assertEquals(makeMeasure("ab")("a"), 1);
* assertEquals(makeMeasure("ab")("aa"), 1);
* assertEquals(makeMeasure("ab")("abc"), 1);
* assertEquals(makeMeasure("ab")(""), Infinity);
* assertEquals(makeMeasure("ab")("bac"), 2);
* assertEquals(makeMeasure("ab")("bcd"), 3);
* assertEquals(makeMeasure("ab")("bcde"), Infinity);
* assertEquals(makeMeasure("abc")("abc"), 0);
* assertEquals(makeMeasure("abc")("ac"), 1);
* assertEquals(makeMeasure("abc")("abb"), 1);
* assertEquals(makeMeasure("abc")("aabc"), 1);
* assertEquals(makeMeasure("abc")("a"), 2);
* assertEquals(makeMeasure("abc")("acb"), 2);
* assertEquals(makeMeasure("abc")("aabcd"), 2);
* assertEquals(makeMeasure("abc")(""), Infinity);
* assertEquals(makeMeasure("abc")("bca"), 3);
* assertEquals(makeMeasure("abcde")("abcde"), 0);
* assertEquals(makeMeasure("abcde")("aBCDe"), 0);
* assertEquals(makeMeasure("abcde")("abcd"), 1);
* assertEquals(makeMeasure("abcde")("aabcde"), 1);
* assertEquals(makeMeasure("abcde")("abcdee"), 1);
* assertEquals(makeMeasure("abcde")("ab?de"), 1);
* assertEquals(makeMeasure("abcde")("abXXde"), 2);
* assertEquals(makeMeasure("abcde")("ae"), 3);
* assertEquals(makeMeasure("abcde")("aedcb"), Infinity);
* assertEquals(makeMeasure("abcde")(""), Infinity);
* assertEquals(makeMeasure("ab de")("abcde"), 0);
* assertEquals(makeMeasure("ab de")("abccde"), 0);
* assertEquals(makeMeasure("ab de")("abXXXXXXXde"), 0);
* assertEquals(makeMeasure("ab de")("abcccccxe"), 1);
* assertEquals(makeMeasure("ab de")(""), Infinity);
* assertEquals(makeMeasure("漢字文字列")("漢字文字列"), 0);
* assertEquals(makeMeasure("漢字文字列")("漢字の文字列"), 1);
* assertEquals(makeMeasure("漢字文字列")("漢字文字"), 1);
* assertEquals(makeMeasure("漢字文字列")("漢字文字烈"), 1);
* assertEquals(makeMeasure("漢字文字列")("漢字辞典"), 3);
* assertEquals(makeMeasure("漢字文字列")("漢和辞典"), Infinity);
* assertEquals(makeMeasure("漢字文字列")(""), Infinity);
* assertEquals(makeMeasure("emoji✅")("emoji✅"), 0);
* assertEquals(makeMeasure("emoji✅")("emoj✅"), 1);
* assertEquals(makeMeasure("emoji✅")("emojji✅"), 1);
* assertEquals(makeMeasure("emoji✅")("emoji⬜"), 1);
* assertEquals(makeMeasure("emoji✅")("emmoji⬜"), 2);
* assertEquals(makeMeasure("emoji✅")("emoji⬜✅❌"), 2);
* assertEquals(makeMeasure("emoji✅")(""), Infinity);
* `
*/
export const makeMeasure = (source: string): (text: string) => number => {
if (source === "") return (text: string) => ...text.length; const shiftpat = new Map<string, number>();
let epsilon = 0;
let acceptpat = INITPAT;
for (const char of source) {
if (char === " ") {
epsilon |= acceptpat;
} else {
for (
) {
const pat = (shiftpat.get(i) ?? 0) | acceptpat;
shiftpat.set(i, pat);
}
acceptpat >>>= 1;
}
}
return (text: string): number => {
for (const char of text) {
const mask = shiftpat.get(char) ?? 0;
i3 = (i3 & epsilon) | ((i3 & mask) >>> 1) | (i2 >>> 1) | i2;
i2 = (i2 & epsilon) | ((i2 & mask) >>> 1) | (i1 >>> 1) | i1;
i1 = (i1 & epsilon) | ((i1 & mask) >>> 1) | (i0 >>> 1) | i0;
i0 = (i0 & epsilon) | ((i0 & mask) >>> 1);
i1 |= i0 >>> 1;
i2 |= i1 >>> 1;
i3 |= i2 >>> 1;
}
const distance = state.findIndex((i) => (i & acceptpat) !== 0);
return distance < 0 ? Infinity : distance;
};
};