;;;
;;; ebmini-search.l
;;; ebmini書籍検索 for xyzzy
;;;
;;; (c) 2009 Kiichiro Kawano
;;; 無保証です。自身の責任のもとでご利用ください。
;;;
;;;
;;; ◎ebminiのセットアップ
;;; ebmini.exeと必要なDLL、検索用バッチファイルをパスの通ったディレクトリに
;;; 置き、設定ファイルを任意のディレクトリに置く。バッチファイルと設定ファイル
;;; 中でパスを指定している部分(EBMINI_CFGFILEとBookPathの行)は、実際の環境に
;;; 合わせて書き換える。
;;;
;;; ◎プログラムの割り当て
;;; タイトルコメントに★のついている以下の関数が直接呼び出せる関数なので、必要な
;;; ものをキーなどに割り当てる。
;;; ・ebms-config-dialog
;;; 設定ダイアログを開く。
;;; ・ebms-reference
;;; 現在のカーソル位置より後方にある最初の参照リンクを辿る。
;;; ・ebms-back/ebms-forward
;;; 検索結果のヒストリを一つ前へ戻る/先へ進む。
;;; ・ebms-main
;;; メイン関数。デフォルトの書籍を選択して起動。
;;; ・ebms-book01〜
;;; 番号の書籍を直接指定した状態で起動。
;;;
;;; ※ebms-book01以降は、関数を複製して数字の部分を書き換えることで、特定の書籍を
;;; 直接指定して起動する関数を追加可能。
;;;
;;; ◎プログラムの設定
;;; 設定ダイアログで以下の設定をする。
;;;
;;; ○書籍一覧
;;; 編集コマンドで任意の番号に書籍データを割り当てる。
;;; ・書籍名
;;; 書籍の名前を指定。
;;; ・検索用コマンド名
;;; 検索用バッチファイルのファイル名を指定。
;;; ・参照リンクのID文字列
;;; ebmini用設定ファイルの%BeginReferenceや%EndReferenceの項目に記述した、
;;; 各書籍の参照リンク内に表示するID文字列を指定。
;;; ebms-reference関数で参照リンクを辿るのに必要。
;;; (串刺し検索用の設定の場合はそれ自体にID文字列を指定する必要はないが、
;;; 検索結果から各書籍の参照リンクを辿るためには、各書籍を単体で検索する
;;; 設定も別に作っておく必要あり)
;;; ・IMEの状態
;;; 起動時のIMEの挙動を指定。
;;;
;;; ◎作業ファイル
;;; 検索結果とヒストリ用のファイルはテンポラリディレクトリに溜まる仕様。
;;; 以下のファイルを削除すればクリアできる。
;;; ヒストリ情報 ebmini-search.xyzzy.histinfo.txt
;;; ヒストリデータ ebmini-search.xyzzy.histdata.txt
;;; 検索結果 ebdata-xyzzy-????????????.ebr(?は数字)
;;;
(provide "ebmini-search")
(in-package "editor")
(export '(ebms-config-dialog
ebms-reference ebms-back ebms-forward
ebms-main ebms-book01 ebms-book02 ebms-book03 ebms-book04))
;;;. スペシャル変数
(defconstant *ebms-book-max* 40)
(defvar *ebms-book-table* nil)
(defvar *ebms-book-name-choice* nil)
(define-history-variable *ebms-default-book-number* 1)
(defconstant *ebms-ime-mode-choice* '("変更しない" "OFFにする" "ONにする"))
(defconstant *ebms-ime-mode-nochange* 0)
(defconstant *ebms-ime-mode-off* 1)
(defconstant *ebms-ime-mode-on* 2)
(defconstant *ebms-text-view-choice-template* '("非表示" "標準" "20項目" "256項目" "最大"))
(defconstant *ebms-text-view* '("-" "+" "20" "256" "max"))
(defvar *ebms-text-view-choice* nil)
(define-history-variable *ebms-text-view-index* 1)
(defconstant *ebms-list-view-choice-template* '("非表示" "20項目" "256項目" "最大"))
(defconstant *ebms-list-view* '("-" "20" "256" "max"))
(defvar *ebms-list-view-choice* nil)
(define-history-variable *ebms-list-view-index* 1)
(defconstant *ebms-config-dirname* ".ebmini-search")
(defconstant *ebms-config-filename* "ebmini-search.ini")
(defconstant *ebms-result-filename* "ebdata-xyzzy-")
(defconstant *ebms-result-fileext* ".ebr")
(defconstant *ebms-histinfo-file* "ebmini-search.xyzzy.histinfo.txt")
(defconstant *ebms-histdata-file* "ebmini-search.xyzzy.histdata.txt")
;;;. 設定
;;;.. ダイアログの選択肢にデフォルトマークを付加
(defun ebms-dialog-set-default (index choice)
(setf (nth index choice)
(concat (nth index choice) "*")))
;;;.. ダイアログの選択肢からデフォルトマークを削除
(defun ebms-dialog-unset-default (index choice)
(setf (nth index choice)
(substring (nth index choice) 0 -1)))
;;;.. ダイアログの表示形式選択肢を生成
(defun ebms-make-dialog-view-choice ()
(when (null *ebms-text-view-choice*)
(setq *ebms-text-view-choice* (copy-seq *ebms-text-view-choice-template*))
(ebms-dialog-set-default *ebms-text-view-index* *ebms-text-view-choice*))
(when (null *ebms-list-view-choice*)
(setq *ebms-list-view-choice* (copy-seq *ebms-list-view-choice-template*))
(ebms-dialog-set-default *ebms-list-view-index* *ebms-list-view-choice*)))
;;;.. ダイアログの書籍名選択肢を生成
(defun ebms-make-dialog-book-choice ()
(setq *ebms-book-name-choice*
(do ((i 0 (1+ i)) (result))
((> i *ebms-book-max*) (nreverse result))
(push
(format nil "~2,'0D: ~A"
i (or (first (svref *ebms-book-table* i)) ""))
result)))
(ebms-dialog-set-default *ebms-default-book-number* *ebms-book-name-choice*))
;;;.. 設定ファイルのパスを取得
(defun ebms-config-path ()
(let ((path (format nil "~A~A/"
(append-trail-slash (user-homedir-pathname))
*ebms-config-dirname*)))
(unless (file-exist-p path)
(create-directory path))
path))
;;;.. 設定ファイル中の一行を解釈
(defun ebms-config-read-para (para)
(let ((items) (number) (data))
(setq items (split-string para #\, t))
(when (nth 0 items)
(setq number (parse-integer (nth 0 items) :junk-allowed t))
(if (or (< number 1) (< *ebms-book-max* number))
(setq number nil)
(setq data
(list
(nth 1 items)
(nth 2 items)
(nth 3 items)
(let ((ime-mode (parse-integer (nth 4 items) :junk-allowed t)))
(if (member ime-mode
`(,*ebms-ime-mode-nochange*
,*ebms-ime-mode-off*
,*ebms-ime-mode-on*))
ime-mode
*ebms-ime-mode-nochange*))))))
(values number data)))
;;;.. 設定の読み込み
(defun ebms-config-read ()
(if (null *ebms-book-table*)
(setq *ebms-book-table* (make-vector (1+ *ebms-book-max*)))
(fill *ebms-book-table* nil))
(let ((path (merge-pathnames *ebms-config-filename* (ebms-config-path))))
(if (file-exist-p path)
(with-open-file (fp path :direction :input)
(do ((para (read-line fp nil 'eof)
(read-line fp nil 'eof)))
((eql para 'eof))
(multiple-value-bind (number data)
(ebms-config-read-para para)
(if number
(setf (svref *ebms-book-table* number) data)))))
(message "設定ファイルは未作成です")))
(ebms-make-dialog-book-choice))
;;;.. 設定の書き込み
(defun ebms-config-write ()
(let ((path (merge-pathnames *ebms-config-filename* (ebms-config-path))))
(with-open-file (fp path :direction :output :if-exists :supersede)
(do ((i 1 (1+ i)) (data))
((> i *ebms-book-max*))
(setq data (svref *ebms-book-table* i))
(if (first data)
(format fp "~A,~A,~A,~A,~A~%"
i (first data) (second data) (third data) (fourth data)))))))
;;;.. 項目の編集ダイアログ
(defparameter *ebms-item-edit-dialog-template*
'(dialog 0 0 260 92
(:caption "項目の編集")
(:font 9 "MS Pゴシック")
(:control
(:static nil "書籍名(&B):" #x50020000 4 6 80 10)
(:edit book-name nil #x50810000 88 4 160 12)
(:static nil "検索用コマンド名(&C):" #x50020000 4 20 80 10)
(:edit search-command nil #x50810000 88 18 160 12)
(:static nil "参照リンクのID文字列(&R):" #x50020000 4 34 80 10)
(:edit link-id nil #x50810000 88 32 160 12)
(:static nil "IMEの状態(&I):" #x50020000 4 48 80 10)
(:combobox ime-mode nil #x50010003 88 46 80 40)
(:button IDOK "OK" #x50010001 152 73 50 14)
(:button IDCANCEL "キャンセル" #x50010000 206 73 50 14))))
(defun ebms-item-edit-dialog (book-number)
(let ((data (svref *ebms-book-table* book-number)))
(multiple-value-bind (result data)
(dialog-box *ebms-item-edit-dialog-template*
(list (cons 'book-name (first data))
(cons 'search-command (second data))
(cons 'link-id (third data))
(cons 'ime-mode *ebms-ime-mode-choice*)
(cons 'ime-mode (fourth data)))
nil)
(when result
(list
(cdr (assoc 'book-name data))
(cdr (assoc 'search-command data))
(cdr (assoc 'link-id data))
(position (cdr (assoc 'ime-mode data)) *ebms-ime-mode-choice* :test #'string=))))))
;;;.. ★設定ダイアログ
(defparameter *ebms-config-dialog-template*
'(dialog 0 0 240 200
(:caption "ebmini-searchの設定")
(:font 9 "MS Pゴシック")
(:control
(:static nil "書籍一覧(&B):" #x50020000 4 4 50 10)
(:listbox booklist nil #x50b10001 4 17 178 160)
(:button edit "編集(&E)" #x50010000 186 17 50 14)
(:button clear "消去(&D)" #x50010000 186 34 50 14)
(:button moveup "↑(&[)" #x50010000 186 51 50 14)
(:button movedown "↓(&])" #x50010000 186 68 50 14)
(:button reload "再読込(&R)" #x50010000 186 102 50 14)
(:button save "保存" #x50010001 132 182 50 14)
(:button IDCANCEL "キャンセル" #x50010000 186 182 50 14))))
(defun ebms-config-dialog ()
(interactive)
(if (null *ebms-book-table*)
(ebms-config-read))
(let ((edited nil)
(button)
(book-number *ebms-default-book-number*))
(loop
(multiple-value-bind (result data)
(dialog-box *ebms-config-dialog-template*
(list (cons 'booklist (nthcdr 1 *ebms-book-name-choice*))
(cons 'booklist (1- book-number)))
nil)
(setq button result)
(setq book-number
(position (cdr (assoc 'booklist data)) *ebms-book-name-choice*
:test #'string=)))
(cond
((eq button 'edit)
(let ((data (ebms-item-edit-dialog book-number)))
(when data
(setf (svref *ebms-book-table* book-number) data)
(ebms-make-dialog-book-choice)
(setq edited t))))
((eq button 'clear)
(setf (svref *ebms-book-table* book-number) nil)
(if (= book-number *ebms-default-book-number*)
(setq *ebms-default-book-number* 1))
(ebms-make-dialog-book-choice)
(setq edited t))
((eq button 'moveup)
(when (> book-number 1)
(let ((temp (svref *ebms-book-table* book-number)))
(setf (svref *ebms-book-table* book-number)
(svref *ebms-book-table* (1- book-number)))
(setf (svref *ebms-book-table* (1- book-number)) temp))
(if (= *ebms-default-book-number* book-number)
(decf *ebms-default-book-number*))
(decf book-number)
(ebms-make-dialog-book-choice)
(setq edited t)))
((eq button 'movedown)
(when (< book-number *ebms-book-max*)
(let ((temp (svref *ebms-book-table* book-number)))
(setf (svref *ebms-book-table* book-number)
(svref *ebms-book-table* (1+ book-number)))
(setf (svref *ebms-book-table* (1+ book-number)) temp))
(if (= *ebms-default-book-number* book-number)
(incf *ebms-default-book-number*))
(incf book-number)
(ebms-make-dialog-book-choice)
(setq edited t)))
((eq button 'reload)
(ebms-config-read)
(setq edited nil))
(t
(return))))
(if edited
(if (eq button 'save)
(ebms-config-write)
(ebms-config-read)))))
;;;. 検索
;;;.. テンポラリディレクトリのパスを取得
(defun ebms-temp-path ()
(or (si:getenv "TEMP") (si:getenv "TMP") (ebms-config-path)))
;;;.. 結果ファイルのパスを生成
(defun ebms-result-file-path ()
(merge-pathnames
(format nil "~A~A~A" *ebms-result-filename*
(format-date-string "%y%m%d%H%M%S")
*ebms-result-fileext*)
(ebms-temp-path)))
;;;.. ヒストリ情報ファイルのパスを生成
(defun ebms-histinfo-file-path ()
(merge-pathnames *ebms-histinfo-file* (ebms-temp-path)))
;;;.. ヒストリデータファイルのパスを生成
(defun ebms-histdata-file-path ()
(merge-pathnames *ebms-histdata-file* (ebms-temp-path)))
;;;.. ヒストリへの書き込み
(defun ebms-history-write (result-path)
(let ((histinfo-path (ebms-histinfo-file-path))
(histdata-path (ebms-histdata-file-path))
(hist-file-max 0)
(hist-file-current 0)
(temp-buffer))
(if (file-exist-p histinfo-path)
(with-open-file (fp histinfo-path :direction :input)
(let ((data (read fp nil)))
(setq hist-file-max (or (nth 0 data) 0))
(setq hist-file-current (or (nth 1 data) 0))))
(if (> hist-file-current hist-file-max)
(setq hist-file-current hist-file-max)))
(unwind-protect
(progn
(setq temp-buffer (create-new-buffer "*temp*"))
(set-buffer temp-buffer)
(if (file-exist-p histdata-path)
(insert-file-contents histdata-path))
(goto-char (point-min))
(when (> hist-file-current 0)
(goto-line hist-file-current)
(next-line))
(setq hist-file-current (current-line-number))
(setq hist-file-max hist-file-current)
(insert (format nil "~A~%" result-path))
(delete-region (point) (progn (goto-char (point-max)) (point)))
(write-file histdata-path t))
(when temp-buffer
(delete-buffer temp-buffer)))
(with-open-file (fp histinfo-path :direction :output :if-exists :supersede)
(format fp "(~A ~A)~%" hist-file-max hist-file-current))))
;;;.. 結果ファイルをオープン
(defun ebms-open-result (result-path)
(let ((b (find-buffer "*EB Result*")))
(if b (delete-buffer b))
(find-file result-path)
(rename-buffer "*EB Result*")))
;;;.. コマンド実行
(defun ebms-search-exec (command method text-view-index list-view-index input)
(let ((result-path (ebms-result-file-path)))
(call-process
(format nil "cmd.exe /c ~A ~A ~A ~A ~A ~A"
command
result-path
method
(nth text-view-index *ebms-text-view*)
(nth list-view-index *ebms-list-view*)
input)
:show :hide
:wait t)
(ebms-open-result result-path)
(save-excursion (ebms-history-write result-path))))
;;;.. 検索語内の文字を変換
(let ((conv-list
'(("!" . "")
("\"" . "")
("'" . "")
("," . "")
("-" . "")
("\\." . "")
("\\?" . "")
("__" . "")
("_" . "")
("~" . "")
("・" . ""))))
(defun ebms-conv-search-word (search-word)
(dolist (x conv-list)
(setq search-word (substitute-string search-word (car x) (cdr x))))
(let ((result-word))
(do ((i 0 (1+ i)) (c))
((>= i (length search-word)))
(setq c (schar search-word i))
(setq result-word (concat result-word (string c)))
(and (kanji-char-p c)
(= (mod (char-code c) 256) #x5c)
(setq result-word (concat result-word "\\"))))
result-word)))
;;;.. 検索ダイアログ
(defparameter *ebms-search-dialog-template*
'(dialog 0 0 280 128
(:caption "ebminiで検索")
(:font 9 "MS Pゴシック")
(:control
(:static nil "検索(&S):" #x50020000 4 6 40 10)
(:combobox search nil #x50210842 50 4 160 240)
(:static nil "本文表示(&T):" #x50020000 4 22 40 10)
(:combobox text-view nil #x50010003 50 20 64 60)
(:static nil "一覧表示(&L):" #x50020000 4 38 40 10)
(:combobox list-view nil #x50010003 50 36 64 60)
(:button set-default-view "現在の表示形式をデフォルトにする(&V)" #x50010000 4 52 140 14)
(:static nil "― 書籍の選択 ――――――――――――――――――――――――"
#x50020000 4 80 220 10)
(:static nil "書籍(&B):" #x50020000 4 94 40 10)
(:combobox book-name nil #x50210803 50 92 160 240)
(:button set-default-book "現在の書籍をデフォルトにする(&D)" #x50010000 4 108 140 14)
(:button word "前方(&W)" #x50010001 226 4 50 14)
(:button endword "後方(&E)" #x50010000 226 21 50 14)
(:button phrase "フレーズ(&P)" #x50010000 226 38 50 14)
(:button menu "メニュー(&M)" #x50010000 226 55 50 14)
(:button copyright "著作権(&R)" #x50010000 226 72 50 14)
(:button IDCANCEL "キャンセル" #x50010000 226 89 50 14))))
(defun ebms-search-dialog (book-number clipboard)
(ebms-make-dialog-view-choice)
(let ((button)
(search-word "")
(text-view-index *ebms-text-view-index*)
(list-view-index *ebms-list-view-index*)
(old-ime-mode (get-ime-mode))
(ime-change (fourth (svref *ebms-book-table* book-number))))
(if clipboard
(setq search-word (or (get-clipboard-data) "")))
(cond ((= ime-change *ebms-ime-mode-off*)
(toggle-ime nil))
((= ime-change *ebms-ime-mode-on*)
(toggle-ime t)))
(loop
(multiple-value-bind (result data)
(dialog-box *ebms-search-dialog-template*
(list
(cons 'search *minibuffer-search-string-history*)
(cons 'search search-word)
(cons 'text-view *ebms-text-view-choice*)
(cons 'text-view text-view-index)
(cons 'list-view *ebms-list-view-choice*)
(cons 'list-view list-view-index)
(cons 'book-name (nthcdr 1 *ebms-book-name-choice*))
(cons 'book-name (1- book-number)))
;'((search :non-null "検索文字列を入力して" :enable (word endword phrase))))
nil)
(setq button result)
(setq book-number
(position (cdr (assoc 'book-name data)) *ebms-book-name-choice* :test #'string=))
(setq search-word (cdr (assoc 'search data)))
(setq text-view-index
(position (cdr (assoc 'text-view data)) *ebms-text-view-choice* :test #'string=))
(setq list-view-index
(position (cdr (assoc 'list-view data)) *ebms-list-view-choice* :test #'string=)))
(cond
((eq button 'set-default-book)
(when (/= *ebms-default-book-number* book-number)
(ebms-dialog-unset-default *ebms-default-book-number* *ebms-book-name-choice*)
(ebms-dialog-set-default book-number *ebms-book-name-choice*)
(setq *ebms-default-book-number* book-number)))
((eq button 'set-default-view)
(when (/= *ebms-text-view-index* text-view-index)
(ebms-dialog-unset-default *ebms-text-view-index* *ebms-text-view-choice*)
(ebms-dialog-set-default text-view-index *ebms-text-view-choice*)
(setq *ebms-text-view-index* text-view-index))
(when (/= *ebms-list-view-index* list-view-index)
(ebms-dialog-unset-default *ebms-list-view-index* *ebms-list-view-choice*)
(ebms-dialog-set-default list-view-index *ebms-list-view-choice*)
(setq *ebms-list-view-index* list-view-index)))
(t
(return))))
(toggle-ime old-ime-mode)
(values button book-number search-word text-view-index list-view-index)))
;;;.. 検索メイン
(defun ebms-search-main (default-book-number clipboard)
(interactive)
(if (null *ebms-book-table*)
(ebms-config-read))
(multiple-value-bind (button book-number search-word text-view-index list-view-index)
(ebms-search-dialog default-book-number clipboard)
(when button
(add-history search-word '*minibuffer-search-string-history*)
(setq *last-search-p* t)
(setq *regexp-search* nil)
(setq *last-search-regexp-p* nil)
(setq *last-search-string* ".■")
(setq search-word (ebms-conv-search-word search-word))
(let ((method) (input))
(case button
((word endword phrase)
(cond ((eq button 'word) (setq method "w"))
((eq button 'endword) (setq method "e"))
((eq button 'phrase) (setq method "p")))
(dolist (w (split-string search-word #\ ))
(setq input
(concat input (format nil "-s\"~A\" " w)))))
(menu
(setq method "M")
(setq input "-s\"dummy\""))
(copyright
(setq method "C")
(setq input "-s\"dummy\"")))
(long-operation
(ebms-search-exec
(second (svref *ebms-book-table* book-number))
method
text-view-index
list-view-index
input))))))
;;;.. ★参照リンクを検索
(defun ebms-reference ()
(interactive)
(if (null *ebms-book-table*)
(ebms-config-read))
(if (or (null (scan-buffer ">" :no-dup nil :reverse nil))
(null (scan-buffer "<" :reverse t :tail t)))
(message "参照リンクが見つかりません")
(let ((book-number))
(do ((n 1 (1+ n))
(id))
((> n *ebms-book-max*))
(setq id (third (svref *ebms-book-table* n)))
(if (string/= id "")
(when (looking-for id)
(setq book-number n)
(return))))
(if (null book-number)
(message "無効な参照リンクです")
(let ((pos))
(save-excursion
(scan-buffer "|\\(.*?\\)>" :reverse nil :regexp t)
(setq pos (match-string 1)))
(long-operation
(ebms-search-exec
(second (svref *ebms-book-table* book-number))
"l"
*ebms-text-view-index*
*ebms-list-view-index*
(format nil "-s~A" pos))))))))
;;;.. ヒストリデータの読み出し
(defun ebms-history-read (hist-file-current)
(let ((histdata-path (ebms-histdata-file-path)))
(if (not (file-exist-p histdata-path))
(progn
(message "エラー:ヒストリデータファイルが存在しません")
nil)
(let ((temp-buffer) (result-path))
(unwind-protect
(progn
(setq temp-buffer (create-new-buffer "*temp*"))
(set-buffer temp-buffer)
(insert-file-contents histdata-path)
(goto-line hist-file-current)
(setq result-path (buffer-substring (point) (progn (goto-eol) (point)))))
(when temp-buffer
(delete-buffer temp-buffer)))
result-path))))
;;;.. ヒストリ移動操作
(defun ebms-history-move (backward)
(let ((histinfo-path (ebms-histinfo-file-path)))
(if (not (file-exist-p histinfo-path))
(message "エラー:ヒストリ情報ファイルが存在しません")
(let ((hist-file-max 0)
(hist-file-current 0))
(with-open-file (fp histinfo-path :direction :input)
(let ((data (read fp nil)))
(setq hist-file-max (or (nth 0 data) 0))
(setq hist-file-current (or (nth 1 data) 0))))
(if (or (= hist-file-max 0)
(= hist-file-current 0)
(> hist-file-current hist-file-max))
(message "エラー:ヒストリ情報ファイルの内容に異常があります")
(cond
((and backward (= hist-file-current 1))
(message "これ以上戻れません"))
((and (not backward) (= hist-file-current hist-file-max))
(message "これ以上進めません"))
(t
(setq hist-file-current (+ hist-file-current (if backward -1 1)))
(let ((result-path
(save-excursion (ebms-history-read hist-file-current))))
(if result-path
(ebms-open-result result-path))
(with-open-file (fp histinfo-path :direction :output
:if-exists :supersede)
(format fp "(~A ~A)~%" hist-file-max hist-file-current))))))))))
;;;.. ★戻る
(defun ebms-back ()
(interactive)
(ebms-history-move t))
;;;.. ★進む
(defun ebms-forward ()
(interactive)
(ebms-history-move nil))
;;;. ★メイン関数
;;; デフォルトの書籍を選択して起動
(defun ebms-main (&optional clipboard)
(interactive)
(ebms-search-main *ebms-default-book-number* clipboard))
;;;. ★書籍を直接指定して起動する関数
(defun ebms-book01 (&optional clipboard)
(interactive)
(ebms-search-main 1 clipboard))
(defun ebms-book02 (&optional clipboard)
(interactive)
(ebms-search-main 2 clipboard))
(defun ebms-book03 (&optional clipboard)
(interactive)
(ebms-search-main 3 clipboard))
(defun ebms-book04 (&optional clipboard)
(interactive)
(ebms-search-main 4 clipboard))