はじめに
ハッシュタグは、SNS などで広く利用される、話題やカテゴリを示すキーワードです。
本記事では、Unicode Standard で規定されているハッシュタグの仕様 を元に、ハッシュタグ機能の実装します。この仕様はハッシュタグの共通的な処理を促進する一方で、実装の自由度も一定程度確保しています。
ハッシュタグの仕様
ハッシュタグの仕様について簡単に説明します。
<Hashtag-Identifier> := <Start> <Continue>* (<Medial> <Continue>+)*
`:=` : 右辺が左辺の構文を定義
`<>` : 定義された構文
`*` : 直前の構文が 0 回以上繰り返されることを示す
`+` : 直前の構文が 1 回以上繰り返されることを示す
`()` : グループ化を表す
Start : ハッシュタグの開始文字
Continue : 続く文字
Medial : オプションで使用できる文字
ハッシュタグの仕様には、さまざまなバリエーションが書かれていますが、今回は以下のようにいします。
-
Start
文字は#
-
Continue
文字にはXID_Continue
プロパティにExtended_Pictographic
プロパティ、Emoji_Component
プロパティ、_
、-
、+
を加えたものを含め、#
を除外 -
Medial
は空
ハッシュタグを解析する際には、Start
文字の前に Continue
文字がない場合にのみハッシュタグとして認識することが推奨されます。例えば、foo#bar
ではハッシュタグは認識されませんが、foo #bar
や foo.#bar
ではハッシュタグとして認識されます。
ハッシュタグの仕様について詳しくは以下の記事で説明しています。
ハッシュタグ機能の実装
Vite による開発環境構築
Vite とは
Vite は、2020 年にリリースされた高速な開発環境を提供するフロントエンド・ビルド・ツールで、オリジナルの開発者は Evan You 氏( Vue.js
の作者)です。Vite は、ビルドシステムとしての役割以外にも開発サーバとしての役割も果たします。
互換性について
Vite は Node.js 18+、20+ のバージョンが必要です。ただし、一部のテンプレートではそれ以上のバージョンの Node.js を必要としますので、パッケージマネージャが警告を出した場合はアップグレードしてください。
はじめに | Vite
node
と npm
を使用できるか確認してください。
$ node -v
v20.9.0
$ npm -v
10.1.0
開発環境構築
- ターミナル開き、デスクトップなどの任意のディレクトリに移動
-
npm init vite@latest
コマンドを実行
もし、以下のようなテキストが表示された場合はy
を入力Need to install the following packages: create-vite@5.3.0 Ok to proceed? (y)
-
Project name の項目で任意のプロジェクト名を入力
今回は、プロジェクト名をhashtag
とします。 -
Select a framework の項目で
Vanilla
を選択 -
Select a variant の項目で
TypeScript
を選択
$ npm init vite@latest
? Project name: › hashtag
? Select a framework: › - Use arrow-keys. Return to submit.
❯ Vanilla
? Select a variant: › - Use arrow-keys. Return to submit.
❯ TypeScript
-
hashtag
ディレクトリに移動 -
npm install
コマンドを実行し、package.json
ファイル内に記述されたパッケージ(TypeScript
など)を一括でインストール(node_modules
ディレクトリ とpackage-lock.json
も同時に作成) -
npm run dev
コマンドを実行し、開発サーバを起動 - Web ブラウザで
http://localhost:5173/
にアクセスし、Vite + TypeScriptと書かれたページが表示される確認。
$ cd hashtag
$ npm install
$ npm run dev
以上で、開発環境の構築が完了しました。
実装
foo #bar #foo#foo #b🦰ar foo.#bar
と入力すると、
#bar #foo#foo #b🦰ar foo.#bar
のようにハッシュタグとして認識される部分が青色に色付けされる機能を実装します。
index.html
index.html
は、Vite + TypeScript で構築されたフロントエンドアプリケーションのエントリポイントとして機能します。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
-
<div id="app"></div>
body
内の要素はシンプルで、id 属性が app のdiv
タグだけです。 -
<script type="module" src="/src/main.ts"></script>
Vite は、src
ディレクトリにあるmain.ts
ファイルをエントリポイントとして、JavaScript コードをバンドルし、実行します。
main.ts の編集
main.ts
ファイルを以下のように編集します。
import { setupHashtag } from './hashtag.ts'
let app = document.querySelector<HTMLDivElement>('#app')!;
setupHashtag(app);
hashtag.ts の編集
counter.ts
ファイルのファイル名を hashtag.ts
に変更し、ファイル内のコードを削除します。
型を定義
type TokenType = 'hashtag' | 'nonHashtag';
type Token = {
type: TokenType;
value: string;
};
正規表現を追記
const Start = /#/;
const Continue = /\p{XID_Continue}|\p{Extended_Pictographic}|\p{Emoji_Component}|[_+-]/u;
setupHashtag
関数を追記
export function setupHashtag(element: HTMLDivElement) {
let textarea = document.createElement('textarea');
element.appendChild(textarea);
let output = document.createElement('div');
element.appendChild(output);
textarea.addEventListener('input', (e) => {
const { target } = e;
if (!(target instanceof HTMLTextAreaElement)) {
return;
}
let result: Token[] = tokenize(target.value);
render(output, result);
})
}
tokenize
関数を追記
// 以下の仕様を実装
// - <Hashtag-Identifier> := <Start> <Continue>*
// - Start の前に Continue がない場合にのみハッシュタグとして認識
function tokenize(text: string): Token[] {
const tokens: Token[] = [];
let currentToken = '';
let inHashtag = false;
function addToken(type: TokenType, value: string) {
tokens.push({ type, value });
}
for (const char of text) {
if (Start.test(char)) {
if (inHashtag) {
addToken('hashtag', currentToken);
inHashtag = false;
currentToken = char;
}else{
if (currentToken.length > 0) {
addToken('nonHashtag', currentToken);
}
inHashtag = true;
currentToken = char;
}
} else if (Continue.test(char)) {
currentToken += char;
} else {
if (inHashtag) {
addToken('hashtag', currentToken);
inHashtag = false;
currentToken = char;
} else{
currentToken += char;
}
}
}
// 最後の部分を処理
if (currentToken) {
addToken(inHashtag ? 'hashtag' : 'nonHashtag', currentToken);
}
return tokens;
}
render
関数を追記
function render(element: HTMLDivElement, tokens: Token[]) {
element.innerHTML = '';
tokens.forEach(token => {
if (token.type === 'hashtag') {
const span = document.createElement('span');
span.style.color = '#blue';
span.textContent = token.value;
element.appendChild(span);
} else {
const textNode = document.createTextNode(token.value);
element.appendChild(textNode);
}
});
}
動作確認
ブラウザで textarea に foo #bar #foo#foo #b🦰ar foo.#bar
と入力し、
#bar #foo#foo #b🦰ar foo.#bar
と表示されるか確認してください。
おわりに
本記事で構築したハッシュタグ機能は、基本的な機能を実装していますが、まだ実装していない機能があります。それは、ハッシュタグの NFKC_CF
形式への変換です。
仕様では、比較やマッチングは NFKC_CF
形式へ変換した後に行う必要があると記述されています。例えば、#MötleyCrüe
は #MÖTLEYCRÜE
と一致する必要があります。
今回は色付けの機能のみを実装しましたが、文字を正規化することでより検索機能を強化することができます。