kouno
https://scrapbox.io/files/67b3c89e18767d924a3e8678.jpg
写真のカピバラは伊豆シャボテン公園のカピバラです。家族旅行で撮りました。
カピバラがめっちゃ好きってわけではないです。
ここはkounoのページですCosense Beaver.iconkouno.icon
こうのです
東葛.dev主催
自社toC向けWebサービス開発
エンジニアリングマネージャー
テックリード
ピープルマネジメント
プロジェクトマネジメン
プロダクトマネジメント
Ruby/Ruby on Rails
Java
Kotlin
TypeScript
Vue.js
登壇経験
Kaigi on Rails
2023
2025
JJUG CCC
2022 Fall
2024 Spring
2024 Fall
BuriKaigi
2025
大吉祥寺.pm
2025
スタッフ経験
JJUG CCC
OOC
技術書同人誌博覧会
コアスタッフ
きのこカンファレンス
kouno.icon「オタクではないので......」
kouno.icon「陰(イン)の者なので......」
/icons/hr.icon
code:style.css
.line.section-0.line-title.section-title span.text {
display: inline-block;
background: linear-gradient(to right, #e60000, #f39800, #fff100, #009944, #0068b7, #1d2088, #920783);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
code:script.js
scrapbox.TimeStamp.removeAllFormats();
scrapbox.TimeStamp.addFormat(() => "/icons2/hr.icon");
scrapbox.TimeStamp.addFormat(() => "/icons2/hr4.icon");
scrapbox.TimeStamp.addFormat(() => "/icons/iine.icon");
scrapbox.TimeStamp.addFormat(() => "/icons/わかる.icon");
const Asearch = (function() {
var INITPAT, INITSTATE, MAXCHAR;
INITPAT = 0x80000000;
MAXCHAR = 0x100;
INITSTATE = INITPAT, 0, 0, 0;
Asearch.prototype.isupper = function(c) {
return (c >= 0x41) && (c <= 0x5a);
};
Asearch.prototype.islower = function(c) {
return (c >= 0x61) && (c <= 0x7a);
};
Asearch.prototype.tolower = function(c) {
if (this.isupper(c)) {
return c + 0x20;
} else {
return c;
}
};
Asearch.prototype.toupper = function(c) {
if (this.islower(c)) {
return c - 0x20;
} else {
return c;
}
};
function Asearch(source) {
var c, i, j, len, mask, ref, ref1;
this.source = source;
this.shiftpat = [];
this.epsilon = 0;
this.acceptpat = 0;
mask = INITPAT;
for (c = i = 0, ref = MAXCHAR; 0 <= ref ? i < ref : i > ref; c = 0 <= ref ? ++i : --i) {
this.shiftpatc = 0;
}
ref1 = this.unpack(this.source);
for (j = 0, len = ref1.length; j < len; j++) {
c = ref1j;
if (c === 0x20) {
this.epsilon |= mask;
} else {
this.shiftpatc |= mask;
this.shiftpatthis.toupper(c) |= mask;
this.shiftpatthis.tolower(c) |= mask;
mask >>>= 1;
}
}
this.acceptpat = mask;
return this;
}
Asearch.prototype.state = function(state, str) {
var c, i, i0, i1, i2, i3, len, mask, ref;
if (state == null) {
state = INITSTATE;
}
if (str == null) {
str = '';
}
i0 = state0;
i1 = state1;
i2 = state2;
i3 = state3;
ref = this.unpack(str);
for (i = 0, len = ref.length; i < len; i++) {
c = refi;
mask = this.shiftpatc;
i3 = (i3 & this.epsilon) | ((i3 & mask) >>> 1) | (i2 >>> 1) | i2;
i2 = (i2 & this.epsilon) | ((i2 & mask) >>> 1) | (i1 >>> 1) | i1;
i1 = (i1 & this.epsilon) | ((i1 & mask) >>> 1) | (i0 >>> 1) | i0;
i0 = (i0 & this.epsilon) | ((i0 & mask) >>> 1);
i1 |= i0 >>> 1;
i2 |= i1 >>> 1;
i3 |= i2 >>> 1;
}
return i0, i1, i2, i3;
};
Asearch.prototype.match = function(str, ambig) {
var s;
if (ambig == null) {
ambig = 0;
}
s = this.state(INITSTATE, str);
if (!(ambig < INITSTATE.length)) {
ambig = INITSTATE.length - 1;
}
return (sambig & this.acceptpat) !== 0;
};
Asearch.prototype.unpack = function(str) {
var bytes, c, code, i, len, ref;
bytes = [];
ref = str.split('');
for (i = 0, len = ref.length; i < len; i++) {
c = refi;
code = c.charCodeAt(0);
if (code > 0xFF) {
bytes.push((code & 0xFF00) >>> 8);
}
bytes.push(code & 0xFF);
}
return bytes;
};
return Asearch;
})();
const myProjectName = scrapbox.Project.name;
const projectNames = myProjectName, 'icons','emoji','icons2'
let emojis = [];
const box = $('<div>').addClass('form-group').css("position", "absolute");
const container = $('<div>').addClass('dropdown');
box.append(container);
let items = $('<ul>').addClass('dropdown-menu');
container.append(items);
$('#editor').append(box);
for (const projectName of projectNames) {
fetch(/api/pages/${projectName}?limit=10000, { credentials: 'same-origin'})
.then( res => res.text())
.then( text => {
const data = JSON.parse( text );
const pages = data.pages;
pages.filter( page => (page.image !== null && page.title.match(/^\w\s\-\++$/)))
.forEach( page => {
emojis.push({
name: page.title,
path: page.title,
icon: /api/pages/${projectName}/${page.title}/icon,
})
})
})
}
// TODO: 様々な文字列が来る場合を考慮する
const taberareloo = ( word, list ) => {
const targetWord = word.replace(':', '');
const regStr = targetWord.split('').reduce( (pre, cur) => pre + cur + '.*' ).replace('+', '\\+');
const reg = RegExp(regStr,'i');
return list.filter( item => item.name.match(reg));
}
const asearched = ( word, list ) => {
const targetWord = word.replace(':', '');
const a = new Asearch( targetWord );
const limitCount = Math.floor( targetWord.length/ 4 ) + 1;
let result = [];
for(let i = 0; i <= limitCount; i++){
let matched = list.filter( item => a.match( item.name, i));
let notExisted = matched.filter( item => {
for( let r of result){
if(r.name === item.name){
return false;
}
}
return true;
})
result = ...result, ...notExisted;
}
return result;
}
const fizzSearch = ( word, list ) => {
const a = asearched( word, list );
const b = taberareloo( word, list );
const c = b.filter( item => {
for( let r of a ){
if( r.name == item.name){
return false;
}
}
return true;
})
return ...a, ...c;
}
let stack = "";
const editor = $('#editor');
const open = () => container.addClass("open");
const close = () => {
stack = "";
container.removeClass("open");
}
const replaceText = (text, cursor, emojiPath) => {
cursor.focus();
setTimeout(()=>{
for(let i = 0; i < text.length; i++){
var ke1 = document.createEvent("Events");
ke1.initEvent("keydown", true, true);
ke1.keyCode = ke1.which = 8; // Backspace
cursor.dispatchEvent(ke1);
}
document.execCommand('insertText',null, [${emojiPath}.icon] );
close();
}, 50)
}
editor.keydown( e => {
const key = e.key;
if(key === undefined ) return;
if( stack === "" && key !== ":"){
close();
return
};
if ($('.cursor-line').text().trim() == 'code:'
|| $('.cursor-line .code-block').length == 1) {
close()
return;
}
if ($('.cursor-line').text().trim() == 'table:' || $('tr.cursor-line').length != 0) {
close();
return;
}
if( key === ':' && stack.length !== 0){
let name = stack.replace(':', '');
for(let emoji of emojis){
if( emoji.name === name ){
let cursor = $('#text-input')0;
replaceText(stack + ":", cursor, emoji.path);
return;
}
}
close()
return;
}
const cursor = $('#text-input')0;
if( key.match(/^\w\s\-\:\+$/) ){
stack += e.key;
let focused = $(':focus');
if(focused.is(items.find('li > a'))){
cursor.focus();
}
}
if( stack.length === 2 ){
if( key === " " ){
stack = "";
return;
}
open();
}
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;
const li = $('<li>').addClass('dropdown-item');
const a = $('<a>').attr("tabindex", "0");
const img = $('<img>').attr("src", emoji.icon)
.addClass("icon").css({ height: "17px", float: "left"});
const nameTag = $('<div>').text(" :" + emoji.name + ":");
a.append(img);
a.append(nameTag);
li.append(a);
newItems.append(li);
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', ''));
css[props0] = props1;
});
box.css({
top: ${parseInt(css.top) + parseInt(css.height) + 3}px,
left: ${css.left}px,
});
})
#member