diff options
| author | Shulhan <ms@kilabit.info> | 2021-01-31 04:56:36 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2021-01-31 06:16:54 +0700 |
| commit | 6c7bfd42bc1128f5969e9e40b23d6b828601f7cb (patch) | |
| tree | 8138149fde47f135b965be0837b4f8b83421728f | |
| parent | 734ce643ecbc992834a8f78b44904b82b09bc84b (diff) | |
| download | kamusku-6c7bfd42bc1128f5969e9e40b23d6b828601f7cb.tar.xz | |
all: rewrite the server
This commit move the directClient to different repository called kamusku
and changes the module name from kamusku to kamusd.
| -rw-r--r-- | .gcloudignore | 6 | ||||
| -rw-r--r-- | .gitignore | 12 | ||||
| -rw-r--r-- | LICENSE | 2 | ||||
| -rw-r--r-- | Makefile | 70 | ||||
| -rw-r--r-- | README.adoc | 112 | ||||
| -rw-r--r-- | README.md | 66 | ||||
| -rw-r--r-- | _www/admin.tmpl | 15 | ||||
| -rw-r--r-- | _www/assets/github.svg (renamed from _www-kamusku/assets/github.svg) | 0 | ||||
| -rw-r--r-- | _www/assets/linux.svg (renamed from _www-kamusku/assets/linux.svg) | 0 | ||||
| -rw-r--r-- | _www/assets/macos.svg (renamed from _www-kamusku/assets/macos.svg) | 0 | ||||
| -rw-r--r-- | _www/assets/windows.svg (renamed from _www-kamusku/assets/windows.svg) | 0 | ||||
| -rw-r--r-- | _www/index.html (renamed from _www-kamusku/index.html) | 232 | ||||
| -rw-r--r-- | _www/index.js (renamed from _www-kamusku/index.js) | 6 | ||||
| -rw-r--r-- | _www/kamusku.js (renamed from _www-kamusku/kbbiclient.js) | 6 | ||||
| -rw-r--r-- | active_client.go | 12 | ||||
| -rw-r--r-- | api_client.go | 25 | ||||
| -rw-r--r-- | api_client_test.go | 39 | ||||
| -rw-r--r-- | client.go | 40 | ||||
| -rw-r--r-- | cmd/kamusd/app.yaml (renamed from cmd/www-kamusku/app.yaml) | 2 | ||||
| -rw-r--r-- | cmd/kamusd/main.go (renamed from cmd/www-kamusku/main.go) | 8 | ||||
| -rw-r--r-- | cmd/kamusku-telegram-bot/app.yaml (renamed from cmd/bot-kamusku/app.yaml) | 6 | ||||
| -rw-r--r-- | cmd/kamusku-telegram-bot/main.go (renamed from cmd/bot-kamusku/main.go) | 12 | ||||
| -rw-r--r-- | cmd/kbbi/main.go | 108 | ||||
| -rw-r--r-- | daftar_kata.go | 17 | ||||
| -rw-r--r-- | definisi_kata.go | 94 | ||||
| -rw-r--r-- | definisi_response.go | 21 | ||||
| -rw-r--r-- | dictionary.go | 178 | ||||
| -rw-r--r-- | dictionary_test.go (renamed from kamus_cache_test.go) | 10 | ||||
| -rw-r--r-- | direct_client.go | 386 | ||||
| -rw-r--r-- | direct_client_test.go | 29 | ||||
| -rw-r--r-- | generate.go | 7 | ||||
| -rw-r--r-- | go.mod | 10 | ||||
| -rw-r--r-- | go.sum | 35 | ||||
| -rw-r--r-- | internal/cmd/mergedic/main.go | 2 | ||||
| -rw-r--r-- | kamus_cache.go | 178 | ||||
| -rw-r--r-- | kamusd.go | 26 | ||||
| -rw-r--r-- | kamusd_test.go (renamed from kamusku_test.go) | 18 | ||||
| -rw-r--r-- | kamusku.go | 52 | ||||
| -rw-r--r-- | kata.go | 98 | ||||
| -rw-r--r-- | kata_test.go | 66 | ||||
| -rw-r--r-- | memfs.go | 9 | ||||
| -rw-r--r-- | server.go | 111 | ||||
| -rw-r--r-- | telegram_bot.go | 32 | ||||
| -rw-r--r-- | testdata/entri.html | 408 | ||||
| -rw-r--r-- | testdata/entri_analisa.html | 342 | ||||
| -rw-r--r-- | testdata/kbbi_dasar.html | 707 |
46 files changed, 600 insertions, 3015 deletions
diff --git a/.gcloudignore b/.gcloudignore index 199e6d9..7815628 100644 --- a/.gcloudignore +++ b/.gcloudignore @@ -22,4 +22,8 @@ # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE -*.out
\ No newline at end of file +*.out + +_bin +_www +daftar_kata_dasar @@ -1,13 +1,11 @@ -/_www-kamusku/bin/* -/bot-kamusku -/bot-kamusku-linux-amd64 -/cmd/www-kamusku/static.go +/_www/bin/* +/kamusd +/kamusku-telegram-bot +/cmd/kamusd/static.go /internal/cmd/mergedic/id_ID.dic /internal/cmd/mergedic/id_ID.dic.new /kamus.gob /kamus.gob.new -/kamusku +/kamusd /testdata/kamus.gob /testdata/kamus.gob.new -/www-kamusku -/www-kamusku-linux-amd64 @@ -1,4 +1,4 @@ -Copyright 2020, M. Shulhan (m.shulhan@gmail.com). +Copyright 2020, M. Shulhan (ms@kilabit.info). All rights reserved. Redistribution and use in source and binary forms, with or without @@ -1,64 +1,38 @@ -## Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +## Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. ## Use of this source code is governed by a BSD-style ## license that can be found in the LICENSE file. -.PHONY: all build lint test install release deploy -.PHONY: dev-server +.PHONY: all test check +.PHONY: build-linux_amd64 +.PHONY: deploy deploy-kamusd deploy-telegram-bot +.PHONY: run-kamusd -all: build lint test +BIN_LINUX_AMD64 := _bin/linux_amd64 -build: - go build ./... - -lint: - golangci-lint run --enable-all \ - --disable=dupl \ - --disable=funlen \ - --disable=godox \ - --disable=gomnd \ - --disable=wsl \ - --disable=gocognit \ - --disable=goerr113 \ - --disable=testpackage \ - ./... +all: test check test: - go test ./... + go test -race -p=1 ./... -install: - go install ./cmd/kbbi/ +check: + golangci-lint run ./... -## -## Release tasks -## +build-linux_amd64: + mkdir -p $(BIN_LINUX_AMD64) + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -o $(BIN_LINUX_AMD64)/ ./cmd/... -release: - mkdir -p _content/bin/ - rm _content/bin/kbbi-* - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ./cmd/kbbi && \ - gzip kbbi && \ - mv kbbi.gz _content/bin/kbbi-linux-amd64.gz - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build ./cmd/kbbi && \ - gzip kbbi && \ - mv kbbi.gz _content/bin/kbbi-darwin-amd64.gz - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build ./cmd/kbbi && \ - zip -m kbbi.zip kbbi.exe && \ - mv kbbi.zip _content/bin/kbbi-windows-amd64.zip +deploy: build-linux_amd64 -deploy: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ - go build -o www-kamusku-linux-amd64 ./cmd/www-kamusku/ - rsync --progress ./www-kamusku-linux-amd64 www-kamusku:~/bin/www-kamusku - rsync --progress --recursive ./_www-kamusku/ www-kamusku:~/bin/_www-kamusku/ +deploy-kamusd: + rsync --progress $(BIN_LINUX_AMD64)/kamusd www-kamusku:~/bin/kamusd -deploy-bot: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ - go build -o bot-kbbi-linux-amd64 ./cmd/bot-kbbi/ - rsync --progress ./bot-kbbi-linux-amd64 www-kamusku:~/bin/bot-kbbi +deploy-telegram-bot: + rsync --progress $(BIN_LINUX_AMD64)/kamusku-telegram-bot www-kamusku:~/bin/kamusku-telegram-bot ## -## Development task +## Development tasks. ## -dev-server: - DEBUG=1 go run ./cmd/www-kamusku +run-kamusd: + DEBUG=2 go run ./cmd/kamusd diff --git a/README.adoc b/README.adoc deleted file mode 100644 index d3127b6..0000000 --- a/README.adoc +++ /dev/null @@ -1,112 +0,0 @@ -= kamusku - -Proyek sumber terbuka implementasi antar-muka perintah dan API untuk Kamus -Besar Bahasa Indonesia. - - -== Program kbbi - -Program kbbi yaitu antar-muka untuk mencari definisi dari kata lewat baris -perintah. - -Program ini sangat sederhana, caranya yaitu dengan memberikan kata yang dicari -setelah nama program, misalnya, - ----- -$ kbbi kamus,bahasa ----- - -Maka akan mencetak definisi dari kata "kamus" dan "bahasa" ke layar, - ----- -=== bahasa - Definisi #1: sistem lambang bunyi yang arbitrer, yang digunakan oleh - anggota suatu masyarakat untuk bekerja sama, berinteraksi, dan - mengidentifikasikan diri - Kelas #1: Nomina: kata benda - Kelas #2: Linguistik: - - - Definisi #2: percakapan (perkataan) yang baik; tingkah laku yang baik; sopan santun - Kelas #1: Nomina: kata benda - Contoh #1: baik budi --nya - - ... - -=== kamus - Definisi #1: karya rujukan atau acuan dalam bentuk cetak maupun digital yang - memuat kata dan ungkapan, dapat disusun menurut abjad atau tema, berisi - keterangan tentang makna, pemakaian, atau terjemahan - Kelas #1: Nomina: kata benda - - Definisi #2: buku yang memuat kumpulan istilah atau nama yang disusun - menurut abjad beserta penjelasan tentang makna dan pemakaiannya - Kelas #1: Nomina: kata benda - - ... ----- - -Unduh program KBBI untuk sistem operasi Anda, - -* https://kilabit.info/project/kbbi/bin/kbbi-linux-amd64.gz[Linux 64bit] -* https://kilabit.info/project/kbbi/bin/kbbi-darwin-amd64.gz[macOS 64bit] -* https://kilabit.info/project/kbbi/bin/kbbi-windows-amd64.gz[Windows 64bit] - - -== Bot Telegram - -Dengan tersedianya API, membuka banyak implementasi terbuka lain, salah -satunya yaitu Bot untuk aplikasi Telegram: https://t.me/KamuskuBot - -Untuk saat ini, KamuskuBot hanya punya satu perintah yaitu "/definisi". Cara -menggunakan perintah ini hampir sama dengan program kbbi yaitu dengan -memberikan kata yang dicari, contohnya, - ----- -/definisi kamus,bahasa ----- - - -== KBBI API - -KBBI API adalah jantung dari semua implementasi di atas dan pencarian definisi -kata di bawah. KBBI API dapat diakses menggunakan HTTP lewat URL berikut: -https://kilabit.info/project/kbbi/api - - -=== API Definisi - -HTTP API untuk mencari definisi dari satu atau lebih kata. - -Format permintaan, - ----- -GET /definisi?kata=<string>,... ----- - -Format respons dalam JSON, - ----- -{ - "<string>": { - "dasar": "<string>", - "pesan": "<string>", - "definisi": [{ - "isi": "<string>", - "kelas": [<string>], - "contoh": [<string>] - }, - ... - ] - } -} ----- - -Jika kata tidak ditemukan atau bila kata bukan kata baku, bagian "pesan" akan -berisi keterangan yang menjelaskan galat dari pencarian. - -Berikut contoh pemanggilan API untuk mencari definisi dari kata "kamus", -"bahaza" (kata tidak ditemukan), dan "analisa" (kata tidak baku): - -https://kilabit.info/project/kbbi/api/definisi?kata=kamus,bahaza,analisa - -Sekian, selamat meretas! diff --git a/README.md b/README.md new file mode 100644 index 0000000..329b04f --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# kamusd + +Proyek sumber terbuka implementasi HTTP API untuk Kamus Besar Bahasa +Indonesia. + + +## HTTP API + +Kamusku HTTP API adalah jantung dari semua implementasi di atas dan pencarian +definisi kata di bawah. +Kamusku HTTP API dapat diakses menggunakan HTTP lewat URL berikut: `/api` + + +### API Definisi + +HTTP API untuk mencari definisi dari satu atau lebih kata. + +Format permintaan, + +``` +GET /api/definisi?kata=<string>,... +``` + +Format respon dalam JSON, + +``` +{ + "<string>": { + "dasar": "<string>", + "pesan": "<string>", + "definisi": [{ + "isi": "<string>", + "kelas": [<string>], + "contoh": [<string>] + }, + ... + ] + } +} +``` + +Jika kata tidak ditemukan atau bila kata bukan kata baku, bagian "pesan" akan +berisi keterangan yang menjelaskan galat dari pencarian. + +Berikut contoh pemanggilan API untuk mencari definisi dari kata "kamus", +"bahaza" (kata tidak ditemukan), dan "analisa" (kata tidak baku): + +``` +/api/definisi?kata=kamus,bahaza,analisa +``` + +## KamuskuBot - Bot Telegram + +Dengan tersedianya API, membuka banyak implementasi terbuka lain, salah +satunya yaitu Bot untuk aplikasi Telegram: https://t.me/KamuskuBot + +Untuk saat ini, KamuskuBot hanya punya satu perintah yaitu "/definisi". Cara +menggunakan perintah ini hampir sama dengan program kamusku yaitu dengan +memberikan kata yang dicari, contohnya, + +``` +/definisi kamus,bahasa +``` + + +Sekian, selamat meretas! diff --git a/_www/admin.tmpl b/_www/admin.tmpl new file mode 100644 index 0000000..a275053 --- /dev/null +++ b/_www/admin.tmpl @@ -0,0 +1,15 @@ +<!DOCTYPE html lang="id"> +<html> +<head> + <title>Project KBBI - Admin</title> + + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> +</head> +<body> +<h2>Tembolok kamus</h2> + + + +</body> +</html> diff --git a/_www-kamusku/assets/github.svg b/_www/assets/github.svg index 3899712..3899712 100644 --- a/_www-kamusku/assets/github.svg +++ b/_www/assets/github.svg diff --git a/_www-kamusku/assets/linux.svg b/_www/assets/linux.svg index 4c2bbed..4c2bbed 100644 --- a/_www-kamusku/assets/linux.svg +++ b/_www/assets/linux.svg diff --git a/_www-kamusku/assets/macos.svg b/_www/assets/macos.svg index 701ef59..701ef59 100644 --- a/_www-kamusku/assets/macos.svg +++ b/_www/assets/macos.svg diff --git a/_www-kamusku/assets/windows.svg b/_www/assets/windows.svg index 8995329..8995329 100644 --- a/_www-kamusku/assets/windows.svg +++ b/_www/assets/windows.svg diff --git a/_www-kamusku/index.html b/_www/index.html index a9a5110..ce76842 100644 --- a/_www-kamusku/index.html +++ b/_www/index.html @@ -1,19 +1,16 @@ <!DOCTYPE html lang="id"> <html> <head> - <title>Project KBBI</title> + <title>Project Kamusku</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> - <script type="text/javascript" src="kbbiclient.js"></script> - <script type="text/javascript" src="index.js"></script> + <script type="text/javascript" src="/kamusku.js"></script> + <script type="text/javascript" src="/index.js"></script> <!-- Global site tag (gtag.js) - Google Analytics --> - <script - async - src="https://www.googletagmanager.com/gtag/js?id=UA-2502278-8" - ></script> + <script async src="https://www.googletagmanager.com/gtag/js?id=UA-2502278-8"></script> <script> window.dataLayer = window.dataLayer || [] function gtag() { @@ -108,149 +105,28 @@ </head> <body> <div class="topbar"> - <div class="title"> - Proyek KBBI - </div> - <div class="menu"> - <span class="menu-item"> - <a - href="https://github.com/shuLhan/kbbi" - target="_blank" - title="Sumber kode KBBI di Github" - > - <img src="assets/github.svg" width="24px" /> - </a> - </span> - </div> + <div class="title">Proyek Kamusku</div> + <div class="menu"></div> </div> - <p> - Proyek sumber terbuka implementasi antar-muka perintah dan API - untuk Kamus Besar Bahasa Indonesia. - </p> - <p> - Sumber kode untuk proyek ini dapat diambil di - <a - href="https://github.com/shuLhan/kbbi" - target="_blank" - title="Sumber kode KBBI di Github" - > - tautan berikut - </a> - </p> - <h2>Program kbbi</h2> - <p> - Program kbbi yaitu antar-muka untuk mencari definisi dari kata - lewat baris perintah. - </p> - <p> - Program ini sangat sederhana, cara menggunakannya cukup dengan - memberikan kata yang dicari setelah nama program, misalnya, - </p> - <pre> -$ kbbi kamus,bahasa -</pre - > <p> - maka akan mencetak definisi dari kata "kamus" dan "bahasa" ke - layar, + Proyek sumber terbuka implementasi HTTP API dan antar-muka perintah untuk Kamus Besar Bahasa + Indonesia. </p> - <pre> -=== bahasa - Definisi #1: sistem lambang bunyi yang arbitrer, yang digunakan oleh - anggota suatu masyarakat untuk bekerja sama, berinteraksi, dan - mengidentifikasikan diri - Kelas #1: Nomina: kata benda - Kelas #2: Linguistik: - - Definisi #2: percakapan (perkataan) yang baik; tingkah laku yang baik; sopan santun - Kelas #1: Nomina: kata benda - Contoh #1: baik budi --nya + <h2>HTTP API</h2> - ... - -=== kamus - Definisi #1: karya rujukan atau acuan dalam bentuk cetak maupun digital yang memuat kata dan ungkapan, dapat disusun menurut abjad atau tema, berisi keterangan tentang makna, pemakaian, atau terjemahan - Kelas #1: Nomina: kata benda - - Definisi #2: buku yang memuat kumpulan istilah atau nama yang disusun menurut abjad beserta penjelasan tentang makna dan pemakaiannya - Kelas #1: Nomina: kata benda - - ... -</pre - > - - <p> - Unduh program KBBI untuk sistem operasi Anda, - </p> - - <div class="unduh"> - <a - class="unduh-item" - href="bin/kbbi-linux-amd64.gz" - title="Program kbbi untuk linux 64bit" - > - <img width="32px" src="assets/linux.svg" /> - Linux 64bit - </a> - <a - class="unduh-item" - href="bin/kbbi-darwin-amd64.gz" - title="Program kbbi untuk macOS 64bit" - > - <img width="32px" src="assets/macos.svg" /> - macOS 64bit - </a> - <a - class="unduh-item" - href="bin/kbbi-windows-amd64.zip" - title="Program kbbi untuk Windows 64bit" - > - <img width="32px" src="assets/windows.svg" /> - Windows 64bit - </a> - </div> - - <h2>Bot Telegram</h2> - - <p> - Dengan tersedianya API, membuka banyak implementasi terbuka lain, - salah satunya yaitu Bot untuk aplikasi Telegram: - <a href="https://t.me/KamuskuBot" target="_blank"> - https://t.me/KamuskuBot - </a> - </p> - <p> - Untuk saat ini, KamuskuBot hanya punya satu perintah yaitu - "/definisi". Cara menggunakan perintah ini hampir sama dengan - program kbbi yaitu dengan memberikan kata yang dicari, contohnya, - </p> - <pre> -/definisi kamus,bahasa -</pre - > - - <h2>KBBI API</h2> - - <p> - KBBI API adalah jantung dari semua implementasi di atas dan - pencarian definisi kata di bawah. KBBI API dapat diakses - menggunakan HTTP lewat: https://kilabit.info/project/kbbi/api. - </p> + <p>Kamusku HTTP API adalah jantung dari semua implementasi klien.</p> <h3>API Definisi</h3> <p>HTTP API untuk mencari definisi dari satu atau lebih kata.</p> - <p> - Format permintaan, - </p> + <p>Format permintaan,</p> <pre> GET /definisi?kata=<string>,... </pre > - <p> - Format respons dalam JSON, - </p> + <p>Format respons dalam JSON,</p> <pre> { "<string>": { @@ -265,22 +141,17 @@ GET /definisi?kata=<string>,... ] } } - </pre +</pre > <p> - Jika kata tidak ditemukan atau bila kata bukan kata baku, bagian - "pesan" akan berisi keterangan yang menjelaskan galat dari - pencarian. + Jika kata tidak ditemukan atau bila kata bukan kata baku, bagian "pesan" akan berisi keterangan yang + menjelaskan galat dari pencarian. </p> <p> - Berikut contoh pemanggilan API untuk mencari definisi dari kata - "kamus", "bahaza" (kata tidak ditemukan), dan "analisa" (kata - tidak baku): - <a - href="https://kilabit.info/project/kbbi/api/definisi?kata=kamus,bahaza,analisa" - target="_blank" - > + Berikut contoh pemanggilan API untuk mencari definisi dari kata "kamus", "bahaza" (kata tidak + ditemukan), dan "analisa" (kata tidak baku): + <a href="/api/definisi?kata=kamus,bahaza,analisa" target="_blank"> /api/definisi?kata=kamus,bahaza,analisa </a> </p> @@ -289,23 +160,70 @@ GET /definisi?kata=<string>,... <div class="row"> <div class="f-left"> - <input - type="text" - id="kata" - maxlength="64" - value="kamus,bahaza,analisa" - /> + <input type="text" id="kata" maxlength="64" value="kamus,bahaza,analisa" /> </div> <div class="b-right"> <button onclick="cariDefinisi()">Cari definisi</button> </div> </div> - <p class="note"> - Catatan: Pisahkan kata dengan koma untuk mencari lebih dari satu - kata. - </p> + <p class="note">Catatan: Pisahkan kata dengan koma untuk mencari lebih dari satu kata.</p> <div id="definisi-result"></div> + + <h2>Bot Telegram</h2> + + <p> + Dengan tersedianya API, membuka banyak implementasi terbuka lain, salah satunya yaitu Bot untuk + aplikasi Telegram: + <a href="https://t.me/KamuskuBot" target="_blank"> https://t.me/KamuskuBot </a> + </p> + <p> + Untuk saat ini, KamuskuBot hanya punya satu perintah yaitu "/definisi". Cara menggunakan perintah + ini hampir sama dengan program kamusku yaitu dengan memberikan kata yang dicari, contohnya, + </p> + <pre> +/definisi kamus,bahasa +</pre + > + + <h2>Program kamusku</h2> + + <p>Program kamusku yaitu antar-muka untuk mencari definisi dari kata lewat baris perintah.</p> + <p> + Program ini sangat sederhana, cara menggunakannya cukup dengan memberikan kata yang dicari setelah + nama program, misalnya, + </p> + <pre> +$ kamusku kamus,bahasa +</pre + > + <p>maka akan mencetak definisi dari kata "kamus" dan "bahasa" ke layar,</p> + <pre> +=== bahasa + Definisi #1: sistem lambang bunyi yang arbitrer, yang digunakan oleh + anggota suatu masyarakat untuk bekerja sama, berinteraksi, dan + mengidentifikasikan diri + Kelas #1: Nomina: kata benda + Kelas #2: Linguistik: - + + Definisi #2: percakapan (perkataan) yang baik; tingkah laku yang baik; sopan santun + Kelas #1: Nomina: kata benda + Contoh #1: baik budi --nya + + ... + +=== kamus + Definisi #1: karya rujukan atau acuan dalam bentuk cetak maupun digital yang memuat kata dan ungkapan, dapat disusun menurut abjad atau tema, berisi keterangan tentang makna, pemakaian, atau terjemahan + Kelas #1: Nomina: kata benda + + Definisi #2: buku yang memuat kumpulan istilah atau nama yang disusun menurut abjad beserta penjelasan tentang makna dan pemakaiannya + Kelas #1: Nomina: kata benda + + ... +</pre + > + + <div class="unduh"></div> </body> </html> diff --git a/_www-kamusku/index.js b/_www/index.js index f5f0217..0ab0b10 100644 --- a/_www-kamusku/index.js +++ b/_www/index.js @@ -1,14 +1,14 @@ /** - * Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. + * Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. * Use of this source code is governed by a BSD-style * license that can be found in the LICENSE file. */ -let kbbiClient = new KBBIClient("") +let kamusku = new Kamusku("") function cariDefinisi() { let kata = document.getElementById("kata").value - kbbiClient.getDefinitions(kata, cariDefinisiCallback) + kamusku.getDefinitions(kata, cariDefinisiCallback) } function cariDefinisiCallback(res) { diff --git a/_www-kamusku/kbbiclient.js b/_www/kamusku.js index ab35633..364fa1b 100644 --- a/_www-kamusku/kbbiclient.js +++ b/_www/kamusku.js @@ -1,13 +1,13 @@ /** - * Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. + * Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. * Use of this source code is governed by a BSD-style * license that can be found in the LICENSE file. */ -class KBBIClient { +class Kamusku { constructor(baseURL) { if (baseURL.length === 0) { - baseURL = "https://kilabit.info/project/kbbi" + baseURL = "" } this.baseURL = baseURL } diff --git a/active_client.go b/active_client.go index fed49a6..6e9dbc5 100644 --- a/active_client.go +++ b/active_client.go @@ -1,13 +1,17 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package kamusku +package kamusd + +import ( + "git.sr.ht/~shulhan/kamusku" +) // // activeClient define an interface for an active client. // type activeClient interface { - CariDefinisi(words []string) (res DefinisiResponse, err error) - ListKataDasar() (kataDasar DaftarKata, err error) + Lookup(words []string) (res kamusku.LookupResponse, err error) + ListRootWords() (rootWords kamusku.Words, err error) } diff --git a/api_client.go b/api_client.go index d85f419..10e487a 100644 --- a/api_client.go +++ b/api_client.go @@ -1,15 +1,18 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package kamusku +package kamusd import ( + "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "strings" + + "git.sr.ht/~shulhan/kamusku" ) // @@ -42,8 +45,8 @@ func newAPIClient(url string) (client *apiClient) { // // Lookup the definition of words through server API. // -func (client *apiClient) CariDefinisi(words []string) ( - res DefinisiResponse, err error, +func (client *apiClient) Lookup(words []string) ( + res kamusku.LookupResponse, err error, ) { if len(words) == 0 { return nil, nil @@ -54,19 +57,19 @@ func (client *apiClient) CariDefinisi(words []string) ( req, err := http.NewRequest(http.MethodGet, client.url+pathAPIDefinisi, nil) if err != nil { - return nil, fmt.Errorf("CariDefinisi: %w", err) + return nil, fmt.Errorf("Lookup: %w", err) } req.URL.RawQuery = params.Encode() httpRes, err := client.conn.Do(req) if err != nil { - return nil, fmt.Errorf("CariDefinisi: %w", err) + return nil, fmt.Errorf("Lookup: %w", err) } resBody, err := ioutil.ReadAll(httpRes.Body) if err != nil { - return nil, fmt.Errorf("CariDefinisi: %w", err) + return nil, fmt.Errorf("Lookup: %w", err) } defer httpRes.Body.Close() @@ -75,18 +78,18 @@ func (client *apiClient) CariDefinisi(words []string) ( return res, nil } - err = res.unpack(resBody) + err = json.Unmarshal(resBody, &res) if err != nil { - return nil, fmt.Errorf("CariDefinisi: %w", err) + return nil, fmt.Errorf("Lookup: %w", err) } return res, nil } // -// ListKataDasar list all of the root words in dictionary. +// ListRootWords list all of the root words in dictionary. // -func (client *apiClient) ListKataDasar() (res DaftarKata, err error) { +func (client *apiClient) ListRootWords() (res kamusku.Words, err error) { //TODO: return cached list. return res, nil } diff --git a/api_client_test.go b/api_client_test.go index a7b408a..46dac0c 100644 --- a/api_client_test.go +++ b/api_client_test.go @@ -1,16 +1,17 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package kamusku +package kamusd import ( "testing" + "git.sr.ht/~shulhan/kamusku" "github.com/shuLhan/share/lib/test" ) -func TestApiClient_CariDefinisi_offline(t *testing.T) { +func TestApiClient_Lookup_offline(t *testing.T) { testServer.offline = true client := newAPIClient(testServerAPI) @@ -18,7 +19,7 @@ func TestApiClient_CariDefinisi_offline(t *testing.T) { cases := []struct { desc string words []string - exp DefinisiResponse + exp kamusku.LookupResponse expError string }{{ desc: "With empty input", @@ -28,13 +29,13 @@ func TestApiClient_CariDefinisi_offline(t *testing.T) { }, { desc: "With valid word in cache", words: []string{"mengeja"}, - exp: DefinisiResponse{ + exp: kamusku.LookupResponse{ "mengeja": testKataMengeja, }, }, { desc: "With duplicate words", words: []string{"mengeja", "mengeja"}, - exp: DefinisiResponse{ + exp: kamusku.LookupResponse{ "mengeja": testKataMengeja, }, }} @@ -42,17 +43,17 @@ func TestApiClient_CariDefinisi_offline(t *testing.T) { for _, c := range cases { t.Logf(c.desc) - got, err := client.CariDefinisi(c.words) + got, err := client.Lookup(c.words) if err != nil { test.Assert(t, "error", c.expError, err.Error(), true) continue } - test.Assert(t, "DefinisiResponse", c.exp, got, true) + test.Assert(t, "kamusku.LookupResponse", c.exp, got, true) } } -func TestApiClient_CariDefinisi_online(t *testing.T) { +func TestApiClient_Lookup_online(t *testing.T) { t.Skip() testServer.offline = false @@ -62,31 +63,31 @@ func TestApiClient_CariDefinisi_online(t *testing.T) { cases := []struct { desc string words []string - exp DefinisiResponse + exp kamusku.LookupResponse expError string }{{ desc: "With empty input", }, { desc: "With valid word in cache", words: []string{"mengeja"}, - exp: DefinisiResponse{ + exp: kamusku.LookupResponse{ "mengeja": testKataMengeja, }, }, { desc: "With duplicate words", words: []string{"mengeja", "mengeja"}, - exp: DefinisiResponse{ + exp: kamusku.LookupResponse{ "mengeja": testKataMengeja, }, }, { desc: "With one of the word not in cache", words: []string{"mengeja", "eja"}, - exp: DefinisiResponse{ + exp: kamusku.LookupResponse{ "mengeja": testKataMengeja, - "eja": &Kata{ - Definisi: []*DefinisiKata{{ - Isi: "lafal huruf satu demi satu", - Kelas: []string{"Verba: kata kerja"}, + "eja": &kamusku.Word{ + Definition: []*kamusku.WordDefinition{{ + Value: "lafal huruf satu demi satu", + Classes: []string{"Verba: kata kerja"}, }}, }, }, @@ -95,7 +96,7 @@ func TestApiClient_CariDefinisi_online(t *testing.T) { for _, c := range cases { t.Logf(c.desc) - got, err := client.CariDefinisi(c.words) + got, err := client.Lookup(c.words) if err != nil { test.Assert(t, "error", c.expError, err.Error(), true) continue @@ -105,6 +106,6 @@ func TestApiClient_CariDefinisi_online(t *testing.T) { t.Logf("got: %s = %+v", k, v) } - test.Assert(t, "DefinisiResponse", c.exp, got, true) + test.Assert(t, "LookupResponse", c.exp, got, true) } } @@ -1,11 +1,13 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package kamusku +package kamusd import ( "fmt" + + "git.sr.ht/~shulhan/kamusku" ) // @@ -14,7 +16,7 @@ import ( type Client struct { active activeClient api *apiClient - direct *directClient + kbbic *kamusku.KbbiClient } // @@ -23,7 +25,7 @@ type Client struct { func NewClient() (cl *Client, err error) { cl = &Client{} - cl.direct, err = newDirectClient() + cl.kbbic, err = kamusku.NewKbbiClient() if err != nil { return nil, err } @@ -31,7 +33,7 @@ func NewClient() (cl *Client, err error) { cl.api = newAPIClient("") if cl.IsAuthenticated() { - cl.active = cl.direct + cl.active = cl.kbbic } else { cl.active = cl.api } @@ -40,18 +42,16 @@ func NewClient() (cl *Client, err error) { } // -// CariDefinisi lookup definition of words. +// Lookup lookup definition of words. // -func (cl *Client) CariDefinisi(words []string) ( - res DefinisiResponse, err error, -) { +func (cl *Client) Lookup(words []string) (res kamusku.LookupResponse, err error) { if cl.active != nil { - return cl.active.CariDefinisi(words) + return cl.active.Lookup(words) } - res, err = cl.api.CariDefinisi(words) + res, err = cl.api.Lookup(words) if err != nil { - return cl.direct.CariDefinisi(words) + return cl.kbbic.Lookup(words) } return res, nil @@ -62,20 +62,20 @@ func (cl *Client) CariDefinisi(words []string) ( // server. // func (cl *Client) IsAuthenticated() bool { - return cl.direct.isAuthenticated() + return cl.kbbic.IsAuthenticated() } // -// ListKataDasar list all of the root words in dictionary. +// ListRootWords list all of the root words in dictionary. // -func (cl *Client) ListKataDasar() (res DaftarKata, err error) { +func (cl *Client) ListRootWords() (res kamusku.Words, err error) { if cl.active != nil { - return cl.active.ListKataDasar() + return cl.active.ListRootWords() } - res, err = cl.api.ListKataDasar() + res, err = cl.api.ListRootWords() if err != nil { - return cl.direct.ListKataDasar() + return cl.kbbic.ListRootWords() } return res, nil @@ -86,12 +86,12 @@ func (cl *Client) ListKataDasar() (res DaftarKata, err error) { // server. // func (cl *Client) Login(user, pass string) (err error) { - err = cl.direct.login(user, pass) + err = cl.kbbic.Login(user, pass) if err != nil { return fmt.Errorf("Login: %w", err) } - cl.active = cl.direct + cl.active = cl.kbbic return nil } diff --git a/cmd/www-kamusku/app.yaml b/cmd/kamusd/app.yaml index 580f7ea..876c9f8 100644 --- a/cmd/www-kamusku/app.yaml +++ b/cmd/kamusd/app.yaml @@ -1,5 +1,5 @@ service: default -runtime: go113 +runtime: go115 instance_class: F2 automatic_scaling: max_instances: 1 diff --git a/cmd/www-kamusku/main.go b/cmd/kamusd/main.go index f559594..6362ac3 100644 --- a/cmd/www-kamusku/main.go +++ b/cmd/kamusd/main.go @@ -1,4 +1,4 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -9,11 +9,13 @@ import ( "os" "os/signal" - "github.com/shuLhan/kamusku" + "git.sr.ht/~shulhan/kamusd" ) func main() { - server, err := kamusku.NewServer("") + log.SetPrefix("kamusd: ") + + server, err := kamusd.NewServer("") if err != nil { log.Fatal(err) } diff --git a/cmd/bot-kamusku/app.yaml b/cmd/kamusku-telegram-bot/app.yaml index 74fd1f5..4760016 100644 --- a/cmd/bot-kamusku/app.yaml +++ b/cmd/kamusku-telegram-bot/app.yaml @@ -1,9 +1,9 @@ -service: bot -runtime: go113 +service: telegram-bot +runtime: go115 instance_class: F2 automatic_scaling: max_instances: 1 env_variables: DEBUG: "2" TELEGRAM_TOKEN: "1121465148:AAH9vI-DkHUOPGTmy1Js0dxKSHLYIIkXaIE" - TELEGRAM_WEBHOOK_URL: "https://bot-dot-kamuskubot.df.r.appspot.com" + TELEGRAM_WEBHOOK_URL: "https://kamusku-telegram-bot.df.r.appspot.com" diff --git a/cmd/bot-kamusku/main.go b/cmd/kamusku-telegram-bot/main.go index c2f60e2..f60f4f6 100644 --- a/cmd/bot-kamusku/main.go +++ b/cmd/kamusku-telegram-bot/main.go @@ -1,10 +1,10 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // -// Program bot-kamusku adalah Telegram Bot yang melayani pencarian definisi -// kata menggunakan perintah pesan di Telegram. +// Program kamusku-telegram-bot adalah Telegram Bot yang melayani pencarian +// definisi kata menggunakan perintah pesan di Telegram. // package main @@ -13,14 +13,14 @@ import ( "os" "os/signal" - "github.com/shuLhan/kamusku" + "git.sr.ht/~shulhan/kamusd" ) func main() { - log.SetFlags(0) + log.SetPrefix("kamusku-telegram-bot: ") // Use the token and Webhook URL from environment variables. - tgbot, err := kamusku.NewTelegramBot("", "") + tgbot, err := kamusd.NewTelegramBot("", "") if err != nil { log.Fatal(err) } diff --git a/cmd/kbbi/main.go b/cmd/kbbi/main.go deleted file mode 100644 index 984596c..0000000 --- a/cmd/kbbi/main.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// -// Program kbbi adalah antar-muka perintah untuk Kamus Besar Bahasa Indonesia -// (KBBI) menggunakan API. -// -package main - -import ( - "flag" - "fmt" - "log" - "sort" - - "github.com/shuLhan/kamusku" -) - -func main() { - var ( - isListKataDasar bool - surel string - sandi string - ) - - log.SetFlags(0) - log.SetPrefix("kbbi") - - flag.StringVar(&surel, "surel", "", "Nama pengguna") - flag.StringVar(&sandi, "sandi", "", "Sandi pengguna") - flag.BoolVar(&isListKataDasar, "daftar-kata-dasar", false, - "Ambil dan cetak semua kata dasar") - - flag.Parse() - - cl, err := kamusku.NewClient() - if err != nil { - log.Fatal(err) - } - - if len(surel) > 0 && len(sandi) > 0 { - err = cl.Login(surel, sandi) - if err != nil { - log.Fatal(err) - } - } - - if isListKataDasar { - if cl.IsAuthenticated() { - log.Fatal("opsi -daftar-kata-dasar membutuhkan opsi -surel dan -sandi") - } - listKataDasar(cl) - return - } - - resDefinisi, err := cl.CariDefinisi(flag.Args()) - if err != nil { - log.Fatal(err) - } - - for k, kata := range resDefinisi { - err = kata.Err() - if err != nil { - fmt.Printf("!!! %s: %s\n", k, err) - continue - } - - fmt.Println("===", k) - if len(kata.Pesan) != 0 { - fmt.Println(" " + kata.Pesan) - continue - } - if len(kata.Dasar) > 0 { - fmt.Printf(" Kata dasar: %s\n", kata.Dasar) - } - for x, def := range kata.Definisi { - fmt.Printf(" Definisi #%d: %s\n", x+1, def.Isi) - - for y, nomina := range def.Kelas { - fmt.Printf(" Kelas #%d: %s\n", y+1, nomina) - } - for z, contoh := range def.Contoh { - fmt.Printf(" Contoh #%d: %s\n", z+1, contoh) - } - fmt.Println() - } - } -} - -func listKataDasar(cl *kamusku.Client) { - kataDasar, err := cl.ListKataDasar() - if err != nil { - log.Println(err) - } - - list := make([]string, 0, len(kataDasar)) - - for k := range kataDasar { - list = append(list, k) - } - - sort.Strings(list) - - for _, kata := range list { - fmt.Println(kata) - } -} diff --git a/daftar_kata.go b/daftar_kata.go deleted file mode 100644 index 6232dcd..0000000 --- a/daftar_kata.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package kamusku - -type DaftarKata map[string]struct{} - -// -// merge other map into current map. -// -func (dk DaftarKata) merge(in DaftarKata) DaftarKata { - for k := range in { - dk[k] = struct{}{} - } - return dk -} diff --git a/definisi_kata.go b/definisi_kata.go deleted file mode 100644 index b050c6b..0000000 --- a/definisi_kata.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package kamusku - -import ( - "fmt" - "strings" - - "github.com/shuLhan/share/lib/net/html" - libstrings "github.com/shuLhan/share/lib/strings" -) - -// -// DefinisiKata contains the meaning of word in dictionary, and optional -// attribute for word classifications and examples. -// -type DefinisiKata struct { - Isi string `json:"isi"` - Kelas []string `json:"kelas,omitempty"` - Contoh []string `json:"contoh,omitempty"` -} - -func parseDefinisiKata(in string, li *html.Node) (defKata *DefinisiKata, err error) { - elFont := li.GetFirstChild() - if elFont == nil || elFont.Data != tagNameFont { - return nil, nil - } - elItalic := elFont.GetFirstChild() - if elItalic == nil || elItalic.Data != tagNameItalic { - return nil, nil - } - - defKata = &DefinisiKata{} - - elSpan := elItalic.GetFirstChild() - for elSpan != nil && elSpan.Data == tagNameSpan { - kelas := elSpan.GetAttrValue(attrNameTitle) - if len(kelas) > 0 { - defKata.Kelas = append(defKata.Kelas, kelas) - } - elSpan = elSpan.GetNextSibling() - } - - el := elFont.GetNextSibling() - if el == nil { - return defKata, nil - } - - defKata.Isi = strings.TrimSpace(libstrings.SingleSpace(el.Data)) - - if defKata.Isi == "→" { - defKata.Isi = "" - el = el.GetNextSibling() - if el == nil || el.Data != tagNameAnchor { - return nil, nil - } - el = el.GetFirstChild() - return nil, fmt.Errorf(`%q adalah bentuk tidak baku dari %q`, - in, el.Data) - } - - if defKata.Isi[len(defKata.Isi)-1] != ':' { - return defKata, nil - } - - defKata.Isi = defKata.Isi[:len(defKata.Isi)-1] - - // Parse the example of kata in the next sibling. - el = el.GetNextSibling() - for el != nil { - if el.Data != tagNameFont { - break - } - - elItalic = el.GetFirstChild() - if elItalic.Data != tagNameItalic { - break - } - - elText := elItalic.GetFirstChild() - if elText != nil { - contoh := strings.TrimSpace(elText.Data) - if len(contoh) > 0 && contoh != ";" { - defKata.Contoh = append(defKata.Contoh, elText.Data) - } - } - - el = el.GetNextSibling() - } - - return defKata, nil -} diff --git a/definisi_response.go b/definisi_response.go deleted file mode 100644 index 602d0e4..0000000 --- a/definisi_response.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package kamusku - -import "encoding/json" - -// -// DefinisiResponse is a response from "/definisi" API. -// Its contains mapping of words and their definitions. -// -type DefinisiResponse map[string]*Kata - -func (res *DefinisiResponse) pack() ([]byte, error) { - return json.Marshal(res) -} - -func (res *DefinisiResponse) unpack(v []byte) (err error) { - return json.Unmarshal(v, res) -} diff --git a/dictionary.go b/dictionary.go new file mode 100644 index 0000000..2dd309f --- /dev/null +++ b/dictionary.go @@ -0,0 +1,178 @@ +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package kamusd + +import ( + "bytes" + "encoding/gob" + "errors" + "io/ioutil" + "log" + "os" + "sync" + + "git.sr.ht/~shulhan/kamusku" + libio "github.com/shuLhan/share/lib/io" +) + +const ( + defStorageName = "kamus.gob" +) + +// +// dictionary contains cache of words and its definitions. +// +type dictionary struct { + sync.Mutex + cache map[string]*kamusku.Word + lastSize int + storagePath string +} + +// +// newDictionary create and initialize the cache for dictionary. +// +func newDictionary(storagePath string) (dict *dictionary, err error) { + if len(storagePath) == 0 { + storagePath = defStorageName + } + + dict = &dictionary{ + cache: make(map[string]*kamusku.Word), + storagePath: storagePath, + } + + err = dict.load() + if err != nil { + return nil, err + } + + return dict, nil +} + +// +// lookup the definition of word from cache or nil if not exist. +// +func (dict *dictionary) lookup(word string) (kata *kamusku.Word) { + dict.Lock() + kata = dict.cache[word] + dict.Unlock() + return kata +} + +// +// isChanging will return true if the last cache size is not equal with +// current size. +// +func (dict *dictionary) isChanging() bool { + dict.Lock() + defer dict.Unlock() + return dict.lastSize != len(dict.cache) +} + +// +// load the cached dictionary from storage. +// +func (dict *dictionary) load() (err error) { + dict.Lock() + defer dict.Unlock() + + v, err := ioutil.ReadFile(dict.storagePath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + return err + } + + r := bytes.NewReader(v) + + dec := gob.NewDecoder(r) + err = dec.Decode(&dict.cache) + if err != nil { + return err + } + + // Clean up. Remove all word that contain "→" as definition. + for k, kata := range dict.cache { + for _, def := range kata.Definition { + if def.Value == "→" { + delete(dict.cache, k) + break + } + } + if len(kata.Definition) == 0 { + delete(dict.cache, k) + } + } + + dict.lastSize = len(dict.cache) + + return nil +} + +// +// set save the definition of word into cache. +// +func (dict *dictionary) set(word string, kata *kamusku.Word) { + if len(word) == 0 || kata == nil { + return + } + + dict.Lock() + dict.cache[word] = kata + dict.Unlock() +} + +// +// store the cache to file only if the storage path is set. +// +func (dict *dictionary) store() (err error) { + if len(dict.storagePath) == 0 { + return nil + } + + dict.Lock() + defer dict.Unlock() + + if len(dict.cache) == 0 { + return nil + } + + newStorage := dict.storagePath + ".new" + + f, err := os.Create(newStorage) + if err != nil { + errc := f.Close() + if errc != nil { + log.Println("dictionary: store: ", err) + } + return err + } + + enc := gob.NewEncoder(f) + err = enc.Encode(&dict.cache) + if err != nil { + errc := f.Close() + if errc != nil { + log.Println("dictionary: store: ", err) + } + return err + } + + errc := f.Close() + if errc != nil { + log.Println("dictionary: store: ", err) + } + + err = libio.Copy(dict.storagePath, newStorage) + if err != nil { + return err + } + + dict.lastSize = len(dict.cache) + + return nil +} diff --git a/kamus_cache_test.go b/dictionary_test.go index 09a51cf..b9861fe 100644 --- a/kamus_cache_test.go +++ b/dictionary_test.go @@ -1,8 +1,8 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package kamusku +package kamusd import ( "testing" @@ -10,8 +10,8 @@ import ( "github.com/shuLhan/share/lib/test" ) -func TestKamusCache_store_load(t *testing.T) { - exp, err := newKamusCache(testKamusStorage) +func TestDictionary_store_load(t *testing.T) { + exp, err := newDictionary(testKamusStorage) if err != nil { t.Fatal(err) } @@ -23,7 +23,7 @@ func TestKamusCache_store_load(t *testing.T) { t.Fatal(err) } - got, err := newKamusCache(testKamusStorage) + got, err := newDictionary(testKamusStorage) if err != nil { t.Fatal(err) } diff --git a/direct_client.go b/direct_client.go deleted file mode 100644 index 61824b1..0000000 --- a/direct_client.go +++ /dev/null @@ -1,386 +0,0 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package kamusku - -import ( - "bytes" - "encoding/gob" - "errors" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/http/cookiejar" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/shuLhan/share/lib/debug" - libhttp "github.com/shuLhan/share/lib/http" - "github.com/shuLhan/share/lib/net/html" - "golang.org/x/net/publicsuffix" -) - -const ( - cookieFile = "cookie" - configDir = "kbbi" - maxPageNumber = 501 -) - -// -// directClient for KBBI web using HTTP. -// -type directClient struct { - baseDir string - cookieURL *url.URL - cookies []*http.Cookie - httpc *http.Client -} - -// -// newDirectClient create and initialize new client that connect directly to -// KBBI official website. -// -func newDirectClient() (cl *directClient, err error) { - cookieURL, err := url.Parse(baseURL) - if err != nil { - return nil, fmt.Errorf("newDirectClient: %w", err) - } - - jarOpt := &cookiejar.Options{ - PublicSuffixList: publicsuffix.List, - } - - jar, err := cookiejar.New(jarOpt) - if err != nil { - return nil, fmt.Errorf("newDirectClient: %w", err) - } - - cl = &directClient{ - cookieURL: cookieURL, - httpc: &http.Client{ - Jar: jar, - Timeout: defTimeout, - }, - } - - err = cl.loadCookies() - if err != nil { - return nil, fmt.Errorf("newDirectClient: %w", err) - } - - if cl.cookies != nil { - jar.SetCookies(cookieURL, cl.cookies) - } - - return cl, nil -} - -// -// CariDefinisi dari daftar kata. -// -func (cl *directClient) CariDefinisi(ins []string) ( - res DefinisiResponse, err error, -) { - res = make(DefinisiResponse, len(ins)) - - for _, in := range ins { - _, ok := res[in] - if ok { - continue - } - - kata := &Kata{} - res[in] = kata - - entriURL := baseURL + entriPath + in - httpRes, err := cl.httpc.Get(entriURL) - if err != nil { - kata.err = err - continue - } - - defer httpRes.Body.Close() - - body, err := ioutil.ReadAll(httpRes.Body) - if err != nil { - kata.err = err - continue - } - - if debug.Value >= 2 { - fmt.Printf(">>> HTML body for %s:\n%s", entriURL, body) - } - - err = kata.parseHTMLEntri(in, body) - if err != nil { - kata.err = err - } - - if len(kata.Definisi) == 0 && len(kata.Pesan) == 0 { - kata.Pesan = "Entri tidak ditemukan" - } - } - - return res, nil -} - -// -// ListKataDasar list all of the root words in dictionary. -// -func (cl *directClient) ListKataDasar() (kataDasar DaftarKata, err error) { - params := url.Values{ - paramNameMasukan: []string{paramValueDasar}, - paramNameMasukanLengkap: []string{paramValueDasar}, - } - - urlPage := baseURL + "/Cari/Jenis?" - - kataDasar = make(DaftarKata) - - for pageNumber := 1; pageNumber <= maxPageNumber; pageNumber++ { - params.Set(paramNamePage, strconv.Itoa(pageNumber)) - - req, err := http.NewRequest(http.MethodGet, urlPage+params.Encode(), nil) - if err != nil { - return kataDasar, err - } - - res, err := cl.httpc.Do(req) - if err != nil { - return kataDasar, fmt.Errorf("ListKataDasar: page %d: %w", - pageNumber, err) - } - - defer res.Body.Close() - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return kataDasar, fmt.Errorf("ListKataDasar: page %d: %w", - pageNumber, err) - } - - got, err := cl.parseHTMLKataDasar(body) - if err != nil { - return kataDasar, fmt.Errorf("ListKataDasar: page %d: %w", - pageNumber, err) - } - if len(got) == 0 { - break - } - - kataDasar.merge(got) - - log.Printf("ListKataDasar: halaman %d, jumlah kata %d, total kata %d", - pageNumber, len(got), len(kataDasar)) - } - - return kataDasar, nil -} - -// -// isAuthenticated will return true if the client already login; otherwise it -// will return false. -// -func (cl *directClient) isAuthenticated() bool { - return len(cl.cookies) > 0 -} - -// -// login authenticate the client using username and password. -// -func (cl *directClient) login(surel, sandi string) (err error) { - tokenLogin, err := cl.preLogin() - if err != nil { - return fmt.Errorf("Login: %w", err) - } - - params := url.Values{ - paramNameRequestVerificationToken: []string{tokenLogin}, - paramNamePosel: []string{surel}, - paramNameKataSandi: []string{sandi}, - paramNameIngatSaya: []string{paramValueFalse}, - } - - reqBody := strings.NewReader(params.Encode()) - - req, err := http.NewRequest(http.MethodPost, loginURL, reqBody) - if err != nil { - return fmt.Errorf("Login: %w", err) - } - - req.Header.Set(libhttp.HeaderContentType, libhttp.ContentTypeForm) - - res, err := cl.httpc.Do(req) - if err != nil { - return fmt.Errorf("Login: %w", err) - } - - defer res.Body.Close() - - resBody, err := ioutil.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("Login: %w", err) - } - - if res.StatusCode >= http.StatusBadRequest { - return fmt.Errorf("login: %d %s", res.StatusCode, resBody) - } - - cl.cookies = cl.httpc.Jar.Cookies(cl.cookieURL) - cl.setCookies() - cl.saveCookies() - - return nil -} - -// -// setCookies for HTTP request that need an authentication. -// -func (cl *directClient) setCookies() { - cl.httpc.Jar.SetCookies(cl.cookieURL, cl.cookies) -} - -func (cl *directClient) parseHTMLKataDasar(htmlBody []byte) ( - kataDasar DaftarKata, err error, -) { - iter, err := html.Parse(bytes.NewReader(htmlBody)) - if err != nil { - return nil, err - } - - kataDasar = make(DaftarKata) - - for node := iter.Next(); node != nil; node = iter.Next() { - if !node.IsElement() { - continue - } - if node.Data != tagNameAnchor { - continue - } - hrefValue := node.GetAttrValue(attrNameHref) - if !strings.HasPrefix(hrefValue, entriPath) { - continue - } - k := strings.TrimSpace(node.FirstChild.Data) - kataDasar[k] = struct{}{} - } - - return kataDasar, nil -} - -// -// parseHTMLLogin get the token at the form login. -// -func (cl *directClient) parseHTMLLogin(htmlBody []byte) ( - token string, err error, -) { - iter, err := html.Parse(bytes.NewReader(htmlBody)) - if err != nil { - return "", err - } - - for node := iter.Next(); node != nil; node = iter.Next() { - if !node.IsElement() { - continue - } - if node.Data != tagNameInput { - continue - } - - token := node.GetAttrValue(attrNameValue) - if len(token) > 0 { - return token, nil - } - } - - return "", fmt.Errorf("token login not found") -} - -// -// preLogin initialize the client to get the first cookie. -// -func (cl *directClient) preLogin() (token string, err error) { - req, err := http.NewRequest(http.MethodGet, loginURL, nil) - if err != nil { - return "", err - } - - res, err := cl.httpc.Do(req) - if err != nil { - return "", err - } - - defer res.Body.Close() - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return "", err - } - - token, err = cl.parseHTMLLogin(body) - if err != nil { - return "", err - } - - return token, nil -} - -// -// loadCookies load the KBBI cookies from file. -// -func (cl *directClient) loadCookies() (err error) { - cl.baseDir, err = os.UserConfigDir() - if err != nil { - return fmt.Errorf("loadCookies: %w", err) - } - - f := filepath.Join(cl.baseDir, configDir, cookieFile) - - _, err = os.Stat(f) - if errors.Is(err, os.ErrNotExist) { - return nil - } - - body, err := ioutil.ReadFile(f) - if err != nil { - return fmt.Errorf("loadCookies: %w", err) - } - - dec := gob.NewDecoder(bytes.NewReader(body)) - - err = dec.Decode(&cl.cookies) - if err != nil { - return fmt.Errorf("loadCookies: %w", err) - } - - return nil -} - -// -// saveCookies store the client cookies to the file for future use. -// -func (cl *directClient) saveCookies() { - err := os.MkdirAll(filepath.Join(cl.baseDir, configDir), 0700) - if err != nil { - log.Println("saveCookies:", err) - } - - f := filepath.Join(cl.baseDir, configDir, cookieFile) - - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - err = enc.Encode(cl.cookies) - if err != nil { - log.Println("saveCookies: ", err) - } - - err = ioutil.WriteFile(f, buf.Bytes(), 0600) - if err != nil { - log.Println("saveCookies: ", err) - } -} diff --git a/direct_client_test.go b/direct_client_test.go deleted file mode 100644 index 533fc78..0000000 --- a/direct_client_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package kamusku - -import ( - "io/ioutil" - "testing" -) - -func TestDirectClient_parseHTMLKataDasar(t *testing.T) { - htmlBody, err := ioutil.ReadFile("testdata/kbbi_dasar.html") - if err != nil { - t.Fatal(err) - } - - cl, err := newDirectClient() - if err != nil { - t.Fatal(err) - } - - got, err := cl.parseHTMLKataDasar(htmlBody) - if err != nil { - t.Fatal(err) - } - - t.Logf("Kata dasar: %v", got) -} diff --git a/generate.go b/generate.go deleted file mode 100644 index 5176311..0000000 --- a/generate.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:generate go run ./internal/generate - -package kamusku @@ -1,10 +1,12 @@ -module github.com/shuLhan/kamusku +module git.sr.ht/~shulhan/kamusd -go 1.13 +go 1.15 require ( - github.com/shuLhan/share v0.15.1-0.20200528164210-c03ddce63f66 - golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e + git.sr.ht/~shulhan/kamusku v0.0.0-00010101000000-000000000000 + github.com/shuLhan/share v0.22.1-0.20210124101421-f76dc891e371 ) //replace github.com/shuLhan/share => ../share + +replace git.sr.ht/~shulhan/kamusku => ../kamusku @@ -1,12 +1,33 @@ -github.com/shuLhan/share v0.15.1-0.20200528164210-c03ddce63f66 h1:XsI02DpIEP8ujAd7uRv9DCrnmfwC7NkTwt4fNNNAkOo= -github.com/shuLhan/share v0.15.1-0.20200528164210-c03ddce63f66/go.mod h1:mpa0ub5qmuko/muUlOROOqLCSHKU76GzuAR/sUaSwRo= +git.sr.ht/~shulhan/asciidoctor-go v0.0.0-20201205130914-be765f32b57b/go.mod h1:ejaxKeBMNL5EpP2zjRP4B8zuOr+MM4ZyGwE3y7807WI= +git.sr.ht/~shulhan/asciidoctor-go v0.0.0-20201226102329-36285ff15434/go.mod h1:ejaxKeBMNL5EpP2zjRP4B8zuOr+MM4ZyGwE3y7807WI= +git.sr.ht/~shulhan/ciigo v0.3.0/go.mod h1:Y5FvSiJg88qshoR1ktj4fLzM5sk1pZcV0kJGU8GAuTo= +git.sr.ht/~shulhan/ciigo v0.3.1-0.20210109200358-c23bd42ef521/go.mod h1:DLyaapVphRtqry80iqw+luWAKepHtbDmbvxqFmulcko= +github.com/shuLhan/share v0.20.2-0.20201122173411-e8b3bf5ee6e9/go.mod h1:oBv+CGHG6u4Sa71+nJJJji8mCgPAadywjsB3I3k/b0o= +github.com/shuLhan/share v0.20.2-0.20201205202022-66069b9e49fe/go.mod h1:oBv+CGHG6u4Sa71+nJJJji8mCgPAadywjsB3I3k/b0o= +github.com/shuLhan/share v0.22.0/go.mod h1:u9caerexlcxmPVDttj7PnkxCBDY6yBRTZ+gGR+1tO98= +github.com/shuLhan/share v0.22.1-0.20210109185915-0490a19341d9/go.mod h1:u9caerexlcxmPVDttj7PnkxCBDY6yBRTZ+gGR+1tO98= +github.com/shuLhan/share v0.22.1-0.20210124101421-f76dc891e371 h1:5UPgRXvrL9YmiydMG72xJYE8LuYS1EcjZQrBqhSX064= +github.com/shuLhan/share v0.22.1-0.20210124101421-f76dc891e371/go.mod h1:y4+p5vUmKNNhMMhU6yGgE6QxTgJxA4nv6OOq+cIf7wU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210108172913-0df2131ae363/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/cmd/mergedic/main.go b/internal/cmd/mergedic/main.go index 7cdba38..d9612ca 100644 --- a/internal/cmd/mergedic/main.go +++ b/internal/cmd/mergedic/main.go @@ -1,4 +1,4 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. diff --git a/kamus_cache.go b/kamus_cache.go deleted file mode 100644 index 52eb1f4..0000000 --- a/kamus_cache.go +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package kamusku - -import ( - "bytes" - "encoding/gob" - "errors" - "io/ioutil" - "log" - "os" - "sync" - - libio "github.com/shuLhan/share/lib/io" -) - -const ( - defStorageName = "kamus.gob" -) - -// -// kamusCache contains cache of words and its definitions. -// -type kamusCache struct { - sync.Mutex - cache map[string]*Kata - lastSize int - - storagePath string -} - -// -// newKamusCache create and initialize the cache for dictionary. -// -func newKamusCache(storagePath string) (kamusc *kamusCache, err error) { - if len(storagePath) == 0 { - storagePath = defStorageName - } - - kamusc = &kamusCache{ - cache: make(map[string]*Kata), - storagePath: storagePath, - } - - err = kamusc.load() - if err != nil { - return nil, err - } - - return kamusc, nil -} - -// -// get the definition of word from cache or nil if not exist. -// -func (kamus *kamusCache) get(word string) (kata *Kata) { - kamus.Lock() - kata = kamus.cache[word] - kamus.Unlock() - return kata -} - -// -// isChanging will return true if the last cache size is not equal with -// current size. -// -func (kamus *kamusCache) isChanging() bool { - kamus.Lock() - defer kamus.Unlock() - return kamus.lastSize != len(kamus.cache) -} - -// -// load the cached dictionary from storage. -// -func (kamus *kamusCache) load() (err error) { - kamus.Lock() - defer kamus.Unlock() - - v, err := ioutil.ReadFile(kamus.storagePath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - return err - } - - r := bytes.NewReader(v) - - dec := gob.NewDecoder(r) - err = dec.Decode(&kamus.cache) - if err != nil { - return err - } - - // Clean up. Remove all word that contain "→" as definition. - for k, kata := range kamus.cache { - for _, def := range kata.Definisi { - if def.Isi == "→" { - delete(kamus.cache, k) - break - } - } - if len(kata.Definisi) == 0 { - delete(kamus.cache, k) - } - } - - kamus.lastSize = len(kamus.cache) - - return nil -} - -// -// set save the definition of word into cache. -// -func (kamus *kamusCache) set(word string, kata *Kata) { - if len(word) == 0 || kata == nil { - return - } - - kamus.Lock() - kamus.cache[word] = kata - kamus.Unlock() -} - -// -// store the cache to file only if the storage path is set. -// -func (kamus *kamusCache) store() (err error) { - if len(kamus.storagePath) == 0 { - return nil - } - - kamus.Lock() - defer kamus.Unlock() - - if len(kamus.cache) == 0 { - return nil - } - - newStorage := kamus.storagePath + ".new" - - f, err := os.Create(newStorage) - if err != nil { - errc := f.Close() - if errc != nil { - log.Println("kamusCache: store: ", err) - } - return err - } - - enc := gob.NewEncoder(f) - err = enc.Encode(&kamus.cache) - if err != nil { - errc := f.Close() - if errc != nil { - log.Println("kamusCache: store: ", err) - } - return err - } - - errc := f.Close() - if errc != nil { - log.Println("kamusCache: store: ", err) - } - - err = libio.Copy(kamus.storagePath, newStorage) - if err != nil { - return err - } - - kamus.lastSize = len(kamus.cache) - - return nil -} diff --git a/kamusd.go b/kamusd.go new file mode 100644 index 0000000..b9340e8 --- /dev/null +++ b/kamusd.go @@ -0,0 +1,26 @@ +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +// Package kamusd provide HTTP server API for Kamusku client and Kamusku +// Telegram bot. +// +package kamusd + +import "time" + +const ( + defListen = ":3394" + defServerAPI = "https://kamuskubot.df.r.appspot.com" + defTimeout = 20 * time.Second + + envPort = "PORT" + envKbbiSandi = "KBBI_SANDI" + envKbbiSurel = "KBBI_SUREL" + + jsonEmptyObject = "{}" + + pathAPIDefinisi = "/api/definisi" + paramNameKata = "kata" +) diff --git a/kamusku_test.go b/kamusd_test.go index 9967c1c..ba71aac 100644 --- a/kamusku_test.go +++ b/kamusd_test.go @@ -1,13 +1,15 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package kamusku +package kamusd import ( "log" "os" "testing" + + "git.sr.ht/~shulhan/kamusku" ) const ( @@ -19,12 +21,12 @@ const ( var ( testServer *Server - testKataMengeja = &Kata{ - Dasar: "eja", - Definisi: []*DefinisiKata{{ - Isi: "melafalkan (menyebutkan) huruf-huruf satu demi satu", - Kelas: []string{"Verba: kata kerja"}, - Contoh: []string{ + testKataMengeja = &kamusku.Word{ + Root: "eja", + Definition: []*kamusku.WordDefinition{{ + Value: "melafalkan (menyebutkan) huruf-huruf satu demi satu", + Classes: []string{"Verba: kata kerja"}, + Examples: []string{ `kita ~ kata “dapat” dengan “d-a-p-a-t”`, }, }}, diff --git a/kamusku.go b/kamusku.go deleted file mode 100644 index 151d85d..0000000 --- a/kamusku.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// -// Package kamusku is Go client for Kamus Besar Bahasa Indonesia at -// kbbi.kemdikbud.go.id. -// -package kamusku - -import "time" - -const ( - hostname = "kbbi.kemdikbud.go.id" - baseURL = "https://" + hostname - loginURL = baseURL + "/Account/Login" - entriPath = "/entri/" - - defServerAPI = "https://kamuskubot.df.r.appspot.com" - envPort = "PORT" - pathAPIDefinisi = "/api/definisi" - - attrNameClass = "class" - attrNameHref = "href" - attrNameTitle = "title" - attrNameValue = "value" - - attrValueRootWord = "rootword" - - tagNameAnchor = "a" - tagNameFont = "font" - tagNameHeader2 = "h2" - tagNameInput = "input" - tagNameItalic = "i" - tagNameOrderedList = "ol" - tagNameSpan = "span" - tagNameUnorderedList = "ul" - - paramNameIngatSaya = "IngatSaya" - paramNameKata = "kata" - paramNameKataSandi = "KataSandi" - paramNameMasukan = "masukan" - paramNameMasukanLengkap = "masukanLengkap" - paramNamePage = "page" - paramNamePosel = "Posel" - paramNameRequestVerificationToken = "__RequestVerificationToken" //nolint: gosec - - paramValueDasar = "dasar" - paramValueFalse = "false" - - defTimeout = 20 * time.Second -) diff --git a/kata.go b/kata.go deleted file mode 100644 index 8cf6466..0000000 --- a/kata.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package kamusku - -import ( - "bytes" - - "github.com/shuLhan/share/lib/net/html" -) - -// -// Err return an error from retrieving definition. -// -func (kata *Kata) Err() error { - return kata.err -} - -// -// Kata store the single root word and its definitions. -// -type Kata struct { - Dasar string `json:"dasar,omitempty"` - Definisi []*DefinisiKata `json:"definisi"` - - // Pesan will contains the message when the word is not found or the - // word is informal (kata tidak baku). - Pesan string `json:"pesan,omitempty"` - - err error -} - -// -// parseHTMLEntri parse HTML body from "/entri/<kata>" page to find the -// definition of the word. -// -func (kata *Kata) parseHTMLEntri(in string, htmlBody []byte) (err error) { - iter, err := html.Parse(bytes.NewReader(htmlBody)) - if err != nil { - return err - } - - for node := iter.Next(); node != nil; node = iter.Next() { - if !node.IsElement() { - continue - } - - switch node.Data { - case tagNameHeader2: - kata.parseKataDasar(node) - - case tagNameOrderedList, tagNameUnorderedList: - li := node.GetFirstChild() - for li != nil { - defKata, err := parseDefinisiKata(in, li) - if err != nil { - kata.Pesan = err.Error() - err = nil - break - } - if defKata == nil { - break - } - kata.Definisi = append(kata.Definisi, defKata) - li = li.GetNextSibling() - } - next := node.GetNextSibling() - iter.SetNext(next) - } - } - - return nil -} - -// -// parseKataDasar given an HMTL element "h2" find a possible root word and -// return true; otherwise it will return false. -// -func (kata *Kata) parseKataDasar(h2 *html.Node) bool { - el := h2.GetFirstChild() - if el.Data != tagNameSpan { - return false - } - v := el.GetAttrValue(attrNameClass) - if v != attrValueRootWord { - return false - } - - el = el.GetFirstChild() - if el.Data != tagNameAnchor { - return false - } - el = el.GetFirstChild() - kata.Dasar = el.Data - - return true -} diff --git a/kata_test.go b/kata_test.go deleted file mode 100644 index d471575..0000000 --- a/kata_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package kamusku - -import ( - "io/ioutil" - "testing" - - "github.com/shuLhan/share/lib/test" -) - -func TestKata_parseHTMLEntri(t *testing.T) { - cases := []struct { - infile string - cari string - exp *Kata - }{{ - infile: "testdata/entri.html", - cari: "informasi", - exp: &Kata{ - Definisi: []*DefinisiKata{{ - Isi: "penerangan", - Kelas: []string{"Nomina: kata benda"}, - }, { - Isi: "pemberitahuan; kabar atau berita tentang sesuatu", - Kelas: []string{"Nomina: kata benda"}, - }, { - Isi: "keseluruhan makna yang menunjang amanat yang " + - "terlihat dalam bagian-bagian " + - "amanat itu", - Kelas: []string{ - "Nomina: kata benda", - "Linguistik: -", - }, - }}, - }, - }, { - infile: "testdata/entri_analisa.html", - cari: "analisa", - exp: &Kata{ - Pesan: `"analisa" adalah bentuk tidak baku dari "analisis"`, - }, - }} - - for _, c := range cases { - htmlBody, err := ioutil.ReadFile(c.infile) - if err != nil { - t.Fatal(err) - } - - got := new(Kata) - - err = got.parseHTMLEntri(c.cari, htmlBody) - if err != nil { - t.Fatal(err) - } - - for x, def := range c.exp.Definisi { - test.Assert(t, "Definisi", def, got.Definisi[x], true) - } - - test.Assert(t, c.infile, c.exp, got, true) - } -} diff --git a/memfs.go b/memfs.go new file mode 100644 index 0000000..a506835 --- /dev/null +++ b/memfs.go @@ -0,0 +1,9 @@ +// Copyright 2021, Shulhan <ms@kilabit.info>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package kamusd + +import "github.com/shuLhan/share/lib/memfs" + +var memfsWWW *memfs.MemFS @@ -1,41 +1,37 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package kamusku +package kamusd import ( "context" + "encoding/json" "fmt" "log" "math/rand" - stdhttp "net/http" + "net/http" "os" "strings" "sync" "time" + "git.sr.ht/~shulhan/kamusku" "github.com/shuLhan/share/lib/ascii" "github.com/shuLhan/share/lib/debug" - "github.com/shuLhan/share/lib/http" -) - -const ( - envKbbiSandi = "KBBI_SANDI" - envKbbiSurel = "KBBI_SUREL" - defListen = ":3394" - emptyResponse = "{}" + libhttp "github.com/shuLhan/share/lib/http" + "github.com/shuLhan/share/lib/memfs" ) // -// Server for KBBI with caching and spell checking functionalities. +// Server for kamusku with caching and spell checking functionalities. // type Server struct { - http *http.Server - kamus *kamusCache + httpd *libhttp.Server + kamus *dictionary // The client that forward request to official KBBI server. - forwardc *directClient + kbbic *kamusku.KbbiClient stopped chan bool wg sync.WaitGroup @@ -49,53 +45,53 @@ type Server struct { // NewServer create and initialize the server with optional path to dictionary // storage. // -func NewServer(kamusStorage string) (server *Server, err error) { +func NewServer(dictionaryStorage string) (server *Server, err error) { address := defListen port := os.Getenv(envPort) if len(port) > 0 { address = ":" + port } - opts := &http.ServerOptions{ - Root: "", + httpdOpts := &libhttp.ServerOptions{ + Options: memfs.Options{ + Root: "_www", + Development: debug.Value >= 2, + }, + Memfs: memfsWWW, Address: address, } - if debug.Value > 0 { - opts.Development = true - } - server = &Server{ stopped: make(chan bool, 1), } - server.kamus, err = newKamusCache(kamusStorage) + server.kamus, err = newDictionary(dictionaryStorage) if err != nil { - return nil, fmt.Errorf("http.NewServer: %w", err) + return nil, fmt.Errorf("NewServer: %w", err) } - server.http, err = http.NewServer(opts) + server.httpd, err = libhttp.NewServer(httpdOpts) if err != nil { - return nil, fmt.Errorf("http.NewServer: %w", err) + return nil, fmt.Errorf("NewServer: %w", err) } - server.forwardc, err = newDirectClient() + server.kbbic, err = kamusku.NewKbbiClient() if err != nil { - return nil, fmt.Errorf("http.NewServer: %w", err) + return nil, fmt.Errorf("NewServer: %w", err) } err = server.registerEndpoints() if err != nil { - return nil, fmt.Errorf("http.NewServer: %w", err) + return nil, fmt.Errorf("NewServer: %w", err) } - if !server.forwardc.isAuthenticated() { + if !server.kbbic.IsAuthenticated() { surel := os.Getenv(envKbbiSurel) sandi := os.Getenv(envKbbiSandi) if len(surel) > 0 && len(sandi) > 0 { - err = server.forwardc.login(surel, sandi) + err = server.kbbic.Login(surel, sandi) if err != nil { - return nil, err + return nil, fmt.Errorf("NewServer: %w", err) } } } @@ -108,16 +104,17 @@ func NewServer(kamusStorage string) (server *Server, err error) { // func (server *Server) Start() (err error) { go server.dumpCacheJob() + server.wg.Add(1) - return server.http.Start() + return server.httpd.Start() } // // Shutdown the HTTP server and save the cache for future use. // func (server *Server) Shutdown() (err error) { - err = server.http.Shutdown(context.TODO()) + err = server.httpd.Shutdown(context.TODO()) server.stopped <- true @@ -162,25 +159,21 @@ func (server *Server) dumpCache() { // handleAdmin is endpoint to manage dictionary cache on the web. // func (server *Server) handleAdmin( - httpRes stdhttp.ResponseWriter, - httpReq *stdhttp.Request, - reqBody []byte, + _ http.ResponseWriter, _ *http.Request, _ []byte, ) (resBody []byte, err error) { return resBody, nil } func (server *Server) handleDefinisi( - httpRes stdhttp.ResponseWriter, - httpReq *stdhttp.Request, - reqBody []byte, + _ http.ResponseWriter, httpReq *http.Request, _ []byte, ) (resBody []byte, err error) { paramKata := httpReq.Form.Get(paramNameKata) if len(paramKata) == 0 { - return []byte(emptyResponse), nil + return []byte(jsonEmptyObject), nil } inputs := strings.Split(paramKata, ",") - res := make(DefinisiResponse, len(inputs)) + res := make(kamusku.LookupResponse, len(inputs)) for _, in := range inputs { in = strings.TrimSpace(in) @@ -188,7 +181,7 @@ func (server *Server) handleDefinisi( continue } - kata := server.kamus.get(in) + kata := server.kamus.lookup(in) if kata != nil { res[in] = kata continue @@ -200,9 +193,8 @@ func (server *Server) handleDefinisi( // The word does not exist in cache, retrieve it from official // website. - fwRes, err := server.forwardc.CariDefinisi([]string{in}) + fwRes, err := server.kbbic.Lookup([]string{in}) if err != nil { - kata.err = err continue } @@ -217,30 +209,25 @@ func (server *Server) handleDefinisi( } if len(res) == 0 { - return []byte(emptyResponse), nil + return []byte(jsonEmptyObject), nil } - resBody, err = res.pack() - if err != nil { - return nil, err - } - - return resBody, nil + return json.Marshal(res) } // // registerEndpoints register the API endpoints. // func (server *Server) registerEndpoints() (err error) { - epDefinisi := &http.Endpoint{ - Method: http.RequestMethodGet, + epDefinisi := &libhttp.Endpoint{ + Method: libhttp.RequestMethodGet, Path: pathAPIDefinisi, - RequestType: http.RequestTypeQuery, - ResponseType: http.ResponseTypeJSON, + RequestType: libhttp.RequestTypeQuery, + ResponseType: libhttp.ResponseTypeJSON, Call: server.handleDefinisi, } - err = server.http.RegisterEndpoint(epDefinisi) + err = server.httpd.RegisterEndpoint(epDefinisi) if err != nil { return fmt.Errorf("registerEndpoints %q: %w", pathAPIDefinisi, err) @@ -249,15 +236,15 @@ func (server *Server) registerEndpoints() (err error) { rand.Seed(time.Now().Unix()) pathAdmin := string(ascii.Random([]byte(ascii.LettersNumber), 16)) - epAdmin := &http.Endpoint{ - Method: http.RequestMethodGet, + epAdmin := &libhttp.Endpoint{ + Method: libhttp.RequestMethodGet, Path: pathAdmin, - RequestType: http.RequestTypeQuery, - ResponseType: http.ResponseTypeHTML, + RequestType: libhttp.RequestTypeQuery, + ResponseType: libhttp.ResponseTypeHTML, Call: server.handleAdmin, } - err = server.http.RegisterEndpoint(epAdmin) + err = server.httpd.RegisterEndpoint(epAdmin) if err != nil { return fmt.Errorf("registerEndpoints %q: %w", pathAdmin, err) } diff --git a/telegram_bot.go b/telegram_bot.go index 061bb4c..3892ece 100644 --- a/telegram_bot.go +++ b/telegram_bot.go @@ -1,8 +1,8 @@ -// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved. +// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package kamusku +package kamusd import ( "bytes" @@ -11,6 +11,7 @@ import ( "os" "strings" + "git.sr.ht/~shulhan/kamusku" "github.com/shuLhan/share/api/telegram/bot" ) @@ -110,7 +111,7 @@ func (tgbot *TelegramBot) handleCommandDefinisi(update bot.Update) { daftarKata := strings.Split(msgReq.CommandArgs, ",") - def, err := tgbot.apiClient.CariDefinisi(daftarKata) + def, err := tgbot.apiClient.Lookup(daftarKata) if err != nil { tgbot.sendError(msgReq, "", err.Error()) return @@ -164,34 +165,33 @@ dan bebas dipakai oleh publik. } } -func formatText(definisiKata DefinisiResponse) string { +func formatText(wordDef kamusku.LookupResponse) string { buf := &bytes.Buffer{} - for k, kata := range definisiKata { + for k, kata := range wordDef { fmt.Fprintf(buf, "<b>%s</b>\n", k) - if len(kata.Pesan) > 0 { - fmt.Fprintln(buf, " "+kata.Pesan) + if len(kata.Message) > 0 { + fmt.Fprintln(buf, " "+kata.Message) fmt.Fprintln(buf, "") continue } - if len(kata.Dasar) > 0 { - fmt.Fprintf(buf, " Kata dasar: <i>%s</i>\n\n", - kata.Dasar) + if len(kata.Root) > 0 { + fmt.Fprintf(buf, " Kata dasar: <i>%s</i>\n\n", kata.Root) } - for x, def := range kata.Definisi { - fmt.Fprintf(buf, "‣ Definisi #%d: %s\n", x+1, def.Isi) + for x, def := range kata.Definition { + fmt.Fprintf(buf, "‣ Definisi #%d: %s\n", x+1, def.Value) - if len(def.Kelas) > 0 { + if len(def.Classes) > 0 { fmt.Fprintln(buf, " Kelas kata,") - for _, kelas := range def.Kelas { + for _, kelas := range def.Classes { fmt.Fprintln(buf, " • "+kelas) } } - if len(def.Contoh) > 0 { + if len(def.Examples) > 0 { fmt.Fprintln(buf, " Contoh,") - for _, contoh := range def.Contoh { + for _, contoh := range def.Examples { fmt.Fprintln(buf, " • "+contoh) } } diff --git a/testdata/entri.html b/testdata/entri.html deleted file mode 100644 index 1899950..0000000 --- a/testdata/entri.html +++ /dev/null @@ -1,408 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <meta charset="utf-8" /> - <meta - name="viewport" - content="width=device-width, initial-scale=1.0" - /> - <meta - name="keywords" - content="kbbi, kbbi online, kbbi daring, kbbi dalam jaringan, kbbi 5, kbbi V, kbbi online terbaru, kbbi terbaru, kbbi resmi, Kamus Besar Bahasa Indonesia, Badan Bahasa, Pusat Bahasa, kamus bahasa Indonesia, kamus daring, kamus indonesia," - /> - <link rel="icon" href="/kbbi-daring-3.ico" /> - <title>Hasil Pencarian - KBBI Daring</title> - <link - href="/Content/css?v=DsWRYqffn1l_yiM362JpjeKWGHv3Xp66PuBRKIpyVUU1" - rel="stylesheet" - /> - - <script src="/bundles/modernizr?v=inCVuEFe6J4Q07A0AcRsbJic_UE5MwpRMNGcOtk94TE1"></script> - </head> - <body style="font-family: Verdana, Geneva, Tahoma, sans-serif;"> - <div - class="navbar navbar-inverse navbar-fixed-top" - style="background-color: #110063; border-color: gold;" - > - <div class="container"> - <div class="navbar-header"> - <img - src="/Content/Images/Logo-Tut-Wuri-Handayani-blue.png" - height="40px;" - width="40px;" - style="margin: 5px;" - /> - <button - type="button" - class="navbar-toggle" - data-toggle="collapse" - data-target=".navbar-collapse" - style="background-color: #110063; border-color: gold;" - > - <span class="icon-bar"></span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - </button> - <a - href="/Beranda" - style=" - color: gold; - text-decoration: none; - margin-left: 5px; - margin-right: 5px; - font-size: larger; - " - >KBBI Daring</a - > - </div> - <div - class="navbar-collapse collapse" - style=" - background-color: #110063; - color: gold; - border-top-color: gold; - " - > - <ul class="nav navbar-nav"> - <li><a href="/" style="color: gold;">Cari</a></li> - <li> - <a - href="/Beranda/SeputarLaman" - style="color: gold;" - >Seputar Laman</a - > - </li> - </ul> - <form - action="/Account/LogOff" - class="navbar-right" - id="logoutForm" - method="post" - > - <input - name="__RequestVerificationToken" - type="hidden" - value="CF8U6nmOAJlAY2k60AvI39TUbFkvFxPojfIX5wstWyS2o46yCeF1TmqBO6HgMak8TWbCvm9K2hN-k0egbXfWAsX9x0OC0g2c3LC2C0-dcLzFEK2_ly2KN7J4rtx8LJ4wAl6ye-QpucHX-AN6HAqo3g2" - /> - <ul class="nav navbar-nav navbar-right"> - <li> - <a - href="/Manage" - style="color: gold;" - title="Lakukan Manajemen Akun" - >Halo Shulhan!</a - > - </li> - <li> - <a - href="javascript:document.getElementById('logoutForm').submit()" - style="color: gold;" - >Keluar</a - > - </li> - </ul> - </form> - </div> - </div> - </div> - <div class="container body-content"> - <script src="/bundles/jquery?v=2u0aRenDpYxArEyILB59ETSCA2cfQkSMlxb6jbMBqf81"></script> - <script> - $(function () { - $(".entrisButton").hover( - function () { - $(this).fadeTo(1, 1) - }, - function () { - $(this).fadeTo(1, 0.18) - }, - ) - }) - </script> - <br /> - - <div> - <h4 class="text-center"> - <span - class="glyphicon glyphicon-info-sign text-primary" - ></span> - <b>Halo Shulhan!</b> - Sudahkah Anda mengecek - <a href="/Manage">halaman manajemen akun Anda</a>? Anda - dapat melihat cara membukanya - <a href="/Beranda/Bantuan#pertanyaan-1">di sini</a>. Jika - Anda pernah mengajukan - <a href="/Manage/ProposalDibuat">usulan-usulan</a>, - mungkin usulan-usulan tersebut telah diproses oleh redaksi - kami. - </h4> - <br /> - </div> - - <form - action="/entri/nul" - class="form-horizontal" - id="searchForm" - method="post" - onsubmit="searchText(event)" - role="form" - > - <div class="form-group"> - <div class="col-md-2"></div> - <div class="col-md-8"> - <div class="input-group form-control-max"> - <input - id="textBoxSearch" - name="frasa" - value="informasi" - type="text" - class="form-control form-control-max" - style="margin-top: 1px;" - placeholder="Pencarian..." - /> - <span class="input-group-btn"> - <span - class="btn btn-primary glyphicon glyphicon-search" - onclick="searchText(event)" - ></span> - </span> - </div> - </div> - </div> - <h3 id="errorMessageDiv"></h3> - <script> - String.prototype.contains = function (it) { - return this.indexOf(it) != -1 - } - function searchText(ev) { - var val = $("#textBoxSearch").val() - ev.preventDefault() - if (!val) { - $("#errorMessageDiv").replaceWith( - '<h3 id="errorMessageDiv"><font color="red"><p class="text-center add-margin-top-5"><i>Kotak pencarian tidak boleh kosong</i></p></font></h3>', - ) - } else { - if ( - val.contains(".") || - val.contains("?") || - val.toLowerCase() == "nul" || - val.toLowerCase() == "bin" - ) { - //for non-dependent respond - window.location.href = - "/" + "Cari/Hasil?frasa=" + val - } else { - window.location.href = "/" + "entri/" + val - } - } - } - </script> - </form> - - <hr /> - <h2 style="margin-bottom: 3px;"> - in.for.ma.si - <small - ><span class="entrisButton" - ><a href="../DataDasarEntri/Edit?eid=31762" - ><span - title="Ubah" - class="glyphicon glyphicon-edit text-success" - ></span></a - ><a href="../DataDasarEntri/Copy?eid=31762" - ><span - title="Usulkan entri baru berdasarkan entri ini" - class="glyphicon glyphicon-duplicate" - style="color: darkcyan;" - ></span></a - ><a href="../DataDasarEntri/Details?eid=31762" - ><span - title="Detail" - class="glyphicon glyphicon-list-alt text-info" - ></span></a - ><span - title="Sejarah redaksi - entri ini tidak memiliki sejarah redaksi" - class="glyphicon glyphicon-book" - style="color: lightgrey;" - ></span - ><a href="https://www.google.com/#q=informasi" - ><span - title="Cari di Google" - class="glyphicon glyphicon-search text-primary" - ></span></a></span - ></small> - </h2> - <p> - <a - href="http://tesaurus.kemdikbud.go.id/tematis/lema/informasi" - >⇢ Tesaurus</a - > - </p> - <ol class="last-list-child"> - <li> - <font color="red" - ><i - ><span title="Nomina: kata benda">n</span> - </i></font - >penerangan - </li> - <li> - <font color="red" - ><i - ><span title="Nomina: kata benda">n</span> - </i></font - >pemberitahuan; kabar atau berita tentang sesuatu - </li> - <li> - <font color="red" - ><i - ><span title="Nomina: kata benda">n</span> - <span title="Linguistik: -">Ling</span> - </i></font - >keseluruhan makna yang menunjang amanat yang terlihat - dalam bagian-bagian amanat itu - </li> - <li style="margin -left:-19px"> - <a - href="../DataDasarMakna/Create?eid=31762&number=4" - class="entrisButton" - ><span - title="Usulkan makna baru" - class="glyphicon glyphicon-plus-sign text-success" - ></span - >Usulkan makna baru</a - > - </li> - </ol> - <h4 style="padding-top: 6px;">Kata Turunan</h4> - <ul style="list -style: none;" class="adjusted-par"> - <li> - <a href="../../entri/menginformasikan" - >menginformasikan</a - > - </li> - </ul> - <h4 style="padding-top: 6px;">Gabungan Kata</h4> - <ul style="list -style: none;" class="adjusted-par"> - <li> - <a href="../../entri/informasi%20elektronik" - >informasi elektronik</a - >; - <a href="../../entri/informasi%20gizi">informasi gizi</a>; - <a href="../../entri/informasi%20jabatan" - >informasi jabatan</a - >; - <a href="../../entri/informasi%20karier" - >informasi karier</a - >; - <a href="../../entri/informasi%20meteorologi" - >informasi meteorologi</a - >; - <a href="../../entri/informasi%20pekerjaan" - >informasi pekerjaan</a - >; - <a href="../../entri/informasi%20pendidikan" - >informasi pendidikan</a - >; - <a href="../../entri/informasi%20pornografi" - >informasi pornografi</a - > - </li> - </ul> - <br /><br /> - <h4> - <a href="/DataDasarEntri/Create" class="entrisButton" - ><span - title="Usulkan entri baru" - class="glyphicon glyphicon-plus-sign text-success" - ></span - >Usulkan entri baru</a - > - </h4> - <hr /> - <footer> - <p> - © 2016 - <a href="http://badanbahasa.kemdikbud.go.id/" - >Badan Pengembangan Bahasa dan Perbukuan</a - >, Kementerian Pendidikan dan Kebudayaan Republik - Indonesia - </p> - <p> - Versi luring: - <a - class="btn btn-primary" - href="https://play.google.com/store/apps/details?id=yuku.kbbi5&hl=in" - >Android</a - > - | - <a - class="btn btn-primary" - href="https://itunes.apple.com/app/kamus-besar-bahasa-indonesia/id1173573777" - >iOS</a - > - || - <span title="by: Ian K" - >Versi daring: 2.0.2.0-20191127214052</span - > - </p> - </footer> - </div> - <script src="/bundles/jquery?v=2u0aRenDpYxArEyILB59ETSCA2cfQkSMlxb6jbMBqf81"></script> - - <script src="/bundles/bootstrap?v=7k-mK_Lw6GRA4MkvIrgrWipUHc3KUDohIwN2DDpspCI1"></script> - - <!-- Global site tag (gtag.js) - Google Analytics --> - <script - async - src="https://www.googletagmanager.com/gtag/js?id=UA-128199158-1" - ></script> - <script> - window.dataLayer = window.dataLayer || [] - function gtag() { - dataLayer.push(arguments) - } - gtag("js", new Date()) - - gtag("config", "UA-128199158-1") - </script> - - <script> - function setSelectionRange(input, selectionStart, selectionEnd) { - if (input.setSelectionRange) { - input.focus() - input.setSelectionRange(selectionStart, selectionEnd) - } else if (input.createTextRange) { - var range = input.createTextRange() - range.collapse(true) - range.moveEnd("character", selectionEnd) - range.moveStart("character", selectionStart) - range.select() - } - } - - function setCaretToPos(input, pos) { - setSelectionRange(input, pos, pos) - } - - $(document).ready(function () { - // Catch all events related to changes http://stackoverflow.com/questions/21215049/disable-text-entry-in-input-type-number - $(".number-input").on("change keyup", function () { - var sanitized = $(this) - .val() - .replace(/[^0-9]/g, "") // Remove invalid characters - $(this).val(sanitized) // Update value - }) - - $(function () { - var tb = document.getElementById("textBoxSearch") - if (tb) { - var val = $("#textBoxSearch").val() - var caretPos = val.length - setCaretToPos(tb, caretPos) - } - }) - }) - </script> - </body> -</html> diff --git a/testdata/entri_analisa.html b/testdata/entri_analisa.html deleted file mode 100644 index 9ba807d..0000000 --- a/testdata/entri_analisa.html +++ /dev/null @@ -1,342 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <meta charset="utf-8" /> - <meta - name="viewport" - content="width=device-width, initial-scale=1.0" - /> - <meta - name="keywords" - content="kbbi, kbbi online, kbbi daring, kbbi dalam jaringan, kbbi 5, kbbi V, kbbi online terbaru, kbbi terbaru, kbbi resmi, Kamus Besar Bahasa Indonesia, Badan Bahasa, Pusat Bahasa, kamus bahasa Indonesia, kamus daring, kamus indonesia," - /> - <link rel="icon" href="/kbbi-daring-3.ico" /> - <title>Hasil Pencarian - KBBI Daring</title> - <link - href="/Content/css?v=oq5T2FgFNthYPMx1RHccxOAHAzzHSva0HzZ7iXO7RRY1" - rel="stylesheet" - /> - - <script src="/bundles/modernizr?v=inCVuEFe6J4Q07A0AcRsbJic_UE5MwpRMNGcOtk94TE1"></script> - </head> - <body style="font-family: Verdana, Geneva, Tahoma, sans-serif;"> - <div - class="navbar navbar-inverse navbar-fixed-top" - style="background-color: #110063; border-color: gold;" - > - <div class="container"> - <div class="navbar-header"> - <img - src="/Content/Images/Logo-Tut-Wuri-Handayani-blue.png" - height="40px;" - width="40px;" - style="margin: 5px;" - /> - <button - type="button" - class="navbar-toggle" - data-toggle="collapse" - data-target=".navbar-collapse" - style="background-color: #110063; border-color: gold;" - > - <span class="icon-bar"></span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - </button> - <a - href="/Beranda" - style=" - color: gold; - text-decoration: none; - margin-left: 5px; - margin-right: 5px; - font-size: larger; - " - >KBBI Daring</a - > - </div> - <div - class="navbar-collapse collapse" - style=" - background-color: #110063; - color: gold; - border-top-color: gold; - " - > - <ul class="nav navbar-nav"> - <li><a href="/" style="color: gold;">Cari</a></li> - <li> - <a - href="/Beranda/SeputarLaman" - style="color: gold;" - >Seputar Laman</a - > - </li> - </ul> - <form - action="/Account/LogOff" - class="navbar-right" - id="logoutForm" - method="post" - > - <input - name="__RequestVerificationToken" - type="hidden" - value="S1pOnzWTQn0qbprhG9bMrC2fYckZS9BO8NZO4CE0mS8LRyW7GKyg303gEDWLZpzf0RURhBcZNfNnjlL05N_xcLvnVBWcVeTYn6pgoTQpnmCxicWd01JnnJs-RKDi9e1P6VZPEajoFg2DjZlS91a4Bw2" - /> - <ul class="nav navbar-nav navbar-right"> - <li> - <a - href="/Manage" - style="color: gold;" - title="Lakukan Manajemen Akun" - >Halo Shulhan!</a - > - </li> - <li> - <a - href="javascript:document.getElementById('logoutForm').submit()" - style="color: gold;" - >Keluar</a - > - </li> - </ul> - </form> - </div> - </div> - </div> - <div class="container body-content"> - <script src="/bundles/jquery?v=2u0aRenDpYxArEyILB59ETSCA2cfQkSMlxb6jbMBqf81"></script> - <script> - $(function () { - $(".entrisButton").hover( - function () { - $(this).fadeTo(1, 1) - }, - function () { - $(this).fadeTo(1, 0.18) - }, - ) - }) - </script> - <br /> - - <div> - <h4 class="text-center"> - <span - class="glyphicon glyphicon-info-sign text-primary" - ></span> - <b>Halo Shulhan!</b> - Sudahkah Anda mengecek - <a href="/Manage">halaman manajemen akun Anda</a>? Anda - dapat melihat cara membukanya - <a href="/Beranda/Bantuan#pertanyaan-1">di sini</a>. Jika - Anda pernah mengajukan - <a href="/Manage/ProposalDibuat">usulan-usulan</a>, - mungkin usulan-usulan tersebut telah diproses oleh redaksi - kami. - </h4> - <br /> - </div> - - <form - action="/entri/nul" - class="form-horizontal" - id="searchForm" - method="post" - onsubmit="searchText(event)" - role="form" - > - <div class="form-group"> - <div class="col-md-2"></div> - <div class="col-md-8"> - <div class="input-group form-control-max"> - <input - id="textBoxSearch" - name="frasa" - value="analisa" - type="text" - class="form-control form-control-max" - style="margin-top: 1px;" - placeholder="Pencarian..." - /> - <span class="input-group-btn"> - <span - class="btn btn-primary glyphicon glyphicon-search" - onclick="searchText(event)" - ></span> - </span> - </div> - </div> - </div> - <h3 id="errorMessageDiv"></h3> - <script> - String.prototype.contains = function (it) { - return this.indexOf(it) != -1 - } - function searchText(ev) { - var val = $("#textBoxSearch").val() - ev.preventDefault() - if (!val) { - $("#errorMessageDiv").replaceWith( - '<h3 id="errorMessageDiv"><font color="red"><p class="text-center add-margin-top-5"><i>Kotak pencarian tidak boleh kosong</i></p></font></h3>', - ) - } else { - if ( - val.contains(".") || - val.contains("?") || - val.toLowerCase() == "nul" || - val.toLowerCase() == "bin" - ) { - //for non-dependent respond - window.location.href = - "/" + "Cari/Hasil?frasa=" + val - } else { - window.location.href = "/" + "entri/" + val - } - } - } - </script> - </form> - - <hr /> - <h2 style="margin-bottom: 3px;"> - ana.li.sa - <small - ><span class="entrisButton" - ><a href="../DataDasarEntri/Edit?eid=3476" - ><span - title="Ubah" - class="glyphicon glyphicon-edit text-success" - ></span></a - ><a href="../DataDasarEntri/Copy?eid=3476" - ><span - title="Usulkan entri baru berdasarkan entri ini" - class="glyphicon glyphicon-duplicate" - style="color: darkcyan;" - ></span></a - ><a href="../DataDasarEntri/Details?eid=3476" - ><span - title="Detail" - class="glyphicon glyphicon-list-alt text-info" - ></span></a - ><span - title="Sejarah redaksi - entri ini tidak memiliki sejarah redaksi" - class="glyphicon glyphicon-book" - style="color: lightgrey;" - ></span - ><a href="https://www.google.com/#q=analisa" - ><span - title="Cari di Google" - class="glyphicon glyphicon-search text-primary" - ></span></a></span - ></small> - </h2> - <p> - <a href="http://tesaurus.kemdikbud.go.id/tematis/lema/analisa" - >⇢ Tesaurus</a - > - </p> - <ul style="list-style: none;" class="adjusted-par"> - <li> - <font color="red"><i> </i></font>→ - <a href="../../entri/analisis">analisis</a> - </li> - </ul> - <br /><br /> - <h4> - <a href="/DataDasarEntri/Create" class="entrisButton" - ><span - title="Usulkan entri baru" - class="glyphicon glyphicon-plus-sign text-success" - ></span - >Usulkan entri baru</a - > - </h4> - <hr /> - <footer> - <p> - © 2016 - <a href="http://badanbahasa.kemdikbud.go.id/" - >Badan Pengembangan dan Pembinaan Bahasa</a - >, Kementerian Pendidikan dan Kebudayaan Republik - Indonesia - </p> - <p> - Versi luring: - <a - class="btn btn-primary" - href="https://play.google.com/store/apps/details?id=yuku.kbbi5&hl=in" - >Android</a - > - | - <a - class="btn btn-primary" - href="https://itunes.apple.com/app/kamus-besar-bahasa-indonesia/id1173573777" - >iOS</a - > - || - <span title="by: Ian K" - >Versi daring: 3.0.0.0-20200410085735</span - > - </p> - </footer> - </div> - <script src="/bundles/jquery?v=2u0aRenDpYxArEyILB59ETSCA2cfQkSMlxb6jbMBqf81"></script> - - <script src="/bundles/bootstrap?v=7k-mK_Lw6GRA4MkvIrgrWipUHc3KUDohIwN2DDpspCI1"></script> - - <!-- Global site tag (gtag.js) - Google Analytics --> - <script - async - src="https://www.googletagmanager.com/gtag/js?id=UA-128199158-1" - ></script> - <script> - window.dataLayer = window.dataLayer || [] - function gtag() { - dataLayer.push(arguments) - } - gtag("js", new Date()) - - gtag("config", "UA-128199158-1") - </script> - - <script> - function setSelectionRange(input, selectionStart, selectionEnd) { - if (input.setSelectionRange) { - input.focus() - input.setSelectionRange(selectionStart, selectionEnd) - } else if (input.createTextRange) { - var range = input.createTextRange() - range.collapse(true) - range.moveEnd("character", selectionEnd) - range.moveStart("character", selectionStart) - range.select() - } - } - - function setCaretToPos(input, pos) { - setSelectionRange(input, pos, pos) - } - - $(document).ready(function () { - // Catch all events related to changes http://stackoverflow.com/questions/21215049/disable-text-entry-in-input-type-number - $(".number-input").on("change keyup", function () { - var sanitized = $(this) - .val() - .replace(/[^0-9]/g, "") // Remove invalid characters - $(this).val(sanitized) // Update value - }) - - $(function () { - var tb = document.getElementById("textBoxSearch") - if (tb) { - var val = $("#textBoxSearch").val() - var caretPos = val.length - setCaretToPos(tb, caretPos) - } - }) - }) - </script> - </body> -</html> diff --git a/testdata/kbbi_dasar.html b/testdata/kbbi_dasar.html deleted file mode 100644 index 4bd5170..0000000 --- a/testdata/kbbi_dasar.html +++ /dev/null @@ -1,707 +0,0 @@ -<!DOCTYPE html>
-<html>
-<head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="keywords" content="kbbi, kbbi online, kbbi daring, kbbi dalam jaringan, kbbi 5, kbbi V, kbbi online terbaru, kbbi terbaru, kbbi resmi, Kamus Besar Bahasa Indonesia, Badan Bahasa, Pusat Bahasa, kamus bahasa Indonesia, kamus daring, kamus indonesia," />
- <link rel="icon" href="/kbbi-daring-3.ico" />
- <title>Jenis - KBBI Daring</title>
- <link href="/Content/css?v=DsWRYqffn1l_yiM362JpjeKWGHv3Xp66PuBRKIpyVUU1" rel="stylesheet"/>
-
- <script src="/bundles/modernizr?v=inCVuEFe6J4Q07A0AcRsbJic_UE5MwpRMNGcOtk94TE1"></script>
-
-</head>
-<body style="font-family:Verdana, Geneva, Tahoma, sans-serif">
- <div class="navbar navbar-inverse navbar-fixed-top"
- style="background-color:#110063;border-color:gold">
- <div class="container">
- <div class="navbar-header">
- <img src="/Content/Images/Logo-Tut-Wuri-Handayani-blue.png"
- height="40px;" width="40px;" style="margin:5px;" />
- <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"
- style="background-color:#110063;border-color:gold;">
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </button>
- <a href="/Beranda" style="color:gold;text-decoration:none;margin-left:5px;margin-right:5px;font-size:larger">KBBI Daring</a>
- </div>
- <div class="navbar-collapse collapse"
- style="background-color:#110063;color:gold;border-top-color:gold">
- <ul class="nav navbar-nav">
- <li><a href="/" style="color:gold">Cari</a></li>
- <li><a href="/Beranda/SeputarLaman" style="color:gold">Seputar Laman</a></li>
- </ul>
- <form action="/Account/LogOff" class="navbar-right" id="logoutForm" method="post"><input name="__RequestVerificationToken" type="hidden" value="VxJ0nEPsxOv5dcs7onowFHpKj96ciIx_V-tsPLY3105li6cieBcOCPaG4SamV6etKq_FqvdYc74vQp9meDxVcBpm2IIhdQAA2z6Y7k5j0cA3ERD3nhT0nge31OlYBb_EUwk8S6pAH4rPPz2baY57ig2" /> <ul class="nav navbar-nav navbar-right">
- <li>
- <a href="/Manage" style="color:gold" title="Lakukan Manajemen Akun">Halo Shulhan!</a>
- </li>
- <li><a href="javascript:document.getElementById('logoutForm').submit()" style="color:gold">Keluar</a></li>
- </ul>
-</form>
- </div>
- </div>
- </div>
- <div class="container body-content">
-
-
-
-<br />
-<h2>Daftar Entri Jenis Dasar</h2>
-<br />
-
-<div class="row">
- <div class="col-lg-2">
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1"
- class="btn btn-default btn-xs"
- title="Ke halaman awal">
- awal
- </a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1"
- class="btn btn-default btn-xs"
- title="Ke 100 halaman sebelumnya">
- ◀◀◀
- </a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1"
- class="btn btn-default btn-xs"
- title="Ke 10 halaman sebelumnya">
- ◀◀
- </a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1"
- class="btn btn-default btn-xs"
- title="Ke halaman sebelumnya">
- ◀
- </a>
- </div>
- <div class="col-lg-2">
- <span id="currentPageId">
- Halaman 1 / 501
- </span>
- </div>
- <div class="col-lg-2">
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=2"
- class="btn btn-default btn-xs"
- title="Ke halaman berikutnya">
- ▶
- </a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=11"
- class="btn btn-default btn-xs"
- title="Ke 10 halaman berikutnya">
- ▶▶
- </a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=101"
- class="btn btn-default btn-xs"
- title="Ke 100 halaman berikutnya">
- ▶▶▶
- </a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=501"
- class="btn btn-default btn-xs"
- title="Ke halaman terakhir">
- akhir
- </a>
- </div>
- <div class="col-lg-3">
- <span><i>Hasil Pencarian: 1 - 100 dari 50001</i></span>
- </div>
-</div>
- <br />
-<div class="row">
- <div class="col-md-3">
- <a href="/entri/A">
- A
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/à">
- à
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/a-">
- a-
- </a>
- </div>
- <div class="col-md-3">
- <a href="/Cari/Hasil?frasa=A.Md.">
- A.Md.
- </a>
- </div>
- <div class="col-md-3">
- <a href="/Cari/Hasil?frasa=a.n.">
- a.n.
- </a>
- </div>
- <div class="col-md-3">
- <a href="/Cari/Hasil?frasa=A.P.">
- A.P.
- </a>
- </div>
- <div class="col-md-3">
- <a href="/Cari/Hasil?frasa=A.Pkt.">
- A.Pkt.
- </a>
- </div>
- <div class="col-md-3">
- <a href="/Cari/Hasil?frasa=a.s.">
- a.s.
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/aa">
- aa
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/AAJI">
- AAJI
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/AAL">
- AAL
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/AAU">
- AAU
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/AAUI">
- AAUI
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/AB">
- AB<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/ab-">
- ab-<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/ab">
- ab<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/AB">
- AB<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/ab">
- ab<sup>3</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/AB">
- AB<sup>3</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/AB">
- AB<sup>4</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/aba">
- aba<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/aba">
- aba<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/aba-aba">
- aba-aba
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abaaka">
- abaaka
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abad">
- abad
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abadi">
- abadi
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abadiah">
- abadiah
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abadiat">
- abadiat
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abah">
- abah<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abah">
- abah<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abah-abah">
- abah-abah<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abah-abah">
- abah-abah<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abah-abah">
- abah-abah<sup>3</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abahui">
- abahui
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abai">
- abai<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/Abai">
- Abai<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abaimana">
- abaimana
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abaka">
- abaka
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abaksial">
- abaksial
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abaktinal">
- abaktinal
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abakus">
- abakus<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abakus">
- abakus<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abal">
- abal
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abal-abal">
- abal-abal<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abal-abal">
- abal-abal<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abal-abal">
- abal-abal<sup>3</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abalone">
- abalone
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abampere">
- abampere
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/aban">
- aban
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abandira">
- abandira
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abandonemen">
- abandonemen
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abang">
- abang<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abang">
- abang<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abang">
- abang<sup>3</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abangan">
- abangan<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abangan">
- abangan<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abangda">
- abangda
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abangga">
- abangga
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abap">
- abap
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abar">
- abar
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abas">
- abas
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abas-abas">
- abas-abas
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abasia">
- abasia
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abat">
- abat
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abate">
- abate
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abatis">
- abatis
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abatisasi">
- abatisasi
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abatoar">
- abatoar
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abau">
- abau<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/Abau">
- Abau<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abawi">
- abawi
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abaya">
- abaya
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abbas">
- abbas
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abdas">
- abdas
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abdi">
- abdi
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abdikasi">
- abdikasi
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abdis">
- abdis
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abdomen">
- abdomen
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abdominal">
- abdominal
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abdu">
- abdu
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abdu">
- abdu
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abduksi">
- abduksi<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abduksi">
- abduksi<sup>2</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abduktor">
- abduktor
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abdul">
- abdul
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abdusen">
- abdusen
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abe">
- abe
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abece">
- abece
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/aben">
- aben
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/aber">
- aber
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/aberasi">
- aberasi
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abet">
- abet
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abet">
- abet
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/ABG">
- ABG
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abi">
- abi
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abian">
- abian
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abib">
- abib
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abibliofobia">
- abibliofobia
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abid">
- abid<sup>1</sup>
- </a>
- </div>
- <div class="col-md-3">
- <a href="/entri/abid">
- abid<sup>2</sup>
- </a>
- </div>
-</div>
-<br />
- <div class="btn-group-sm">
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=A" class="btn btn-sm btn-default"><font color="#0060B6">A</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=B" class="btn btn-sm btn-default"><font color="#0060B6">B</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=C" class="btn btn-sm btn-default"><font color="#0060B6">C</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=D" class="btn btn-sm btn-default"><font color="#0060B6">D</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=E" class="btn btn-sm btn-default"><font color="#0060B6">E</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=F" class="btn btn-sm btn-default"><font color="#0060B6">F</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=G" class="btn btn-sm btn-default"><font color="#0060B6">G</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=H" class="btn btn-sm btn-default"><font color="#0060B6">H</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=I" class="btn btn-sm btn-default"><font color="#0060B6">I</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=J" class="btn btn-sm btn-default"><font color="#0060B6">J</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=K" class="btn btn-sm btn-default"><font color="#0060B6">K</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=L" class="btn btn-sm btn-default"><font color="#0060B6">L</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=M" class="btn btn-sm btn-default"><font color="#0060B6">M</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=N" class="btn btn-sm btn-default"><font color="#0060B6">N</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=O" class="btn btn-sm btn-default"><font color="#0060B6">O</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=P" class="btn btn-sm btn-default"><font color="#0060B6">P</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=Q" class="btn btn-sm btn-default"><font color="#0060B6">Q</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=R" class="btn btn-sm btn-default"><font color="#0060B6">R</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=S" class="btn btn-sm btn-default"><font color="#0060B6">S</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=T" class="btn btn-sm btn-default"><font color="#0060B6">T</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=U" class="btn btn-sm btn-default"><font color="#0060B6">U</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=V" class="btn btn-sm btn-default"><font color="#0060B6">V</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=W" class="btn btn-sm btn-default"><font color="#0060B6">W</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=X" class="btn btn-sm btn-default"><font color="#0060B6">X</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=Y" class="btn btn-sm btn-default"><font color="#0060B6">Y</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1&filter=Z" class="btn btn-sm btn-default"><font color="#0060B6">Z</font></a>
- <a href="/Cari/Jenis?masukan=dasar&masukanLengkap=Dasar&page=1" class="btn btn-sm btn-default">Semua</a>
- </div>
-
- <br />
-
-
-
-
- <hr />
- <footer>
- <p>© 2016 <a href="http://badanbahasa.kemdikbud.go.id/">Badan Pengembangan Bahasa dan Perbukuan</a>, Kementerian Pendidikan dan Kebudayaan Republik Indonesia</p>
- <p>Versi luring: <a class="btn btn-primary" href="https://play.google.com/store/apps/details?id=yuku.kbbi5&hl=in">Android</a> | <a class="btn btn-primary" href="https://itunes.apple.com/app/kamus-besar-bahasa-indonesia/id1173573777">iOS</a>
- || <span title="by: Ian K">Versi daring: 2.0.2.0-20191127214052</span></p>
-</footer>
-
- </div>
- <script src="/bundles/jquery?v=2u0aRenDpYxArEyILB59ETSCA2cfQkSMlxb6jbMBqf81"></script>
-
- <script src="/bundles/bootstrap?v=7k-mK_Lw6GRA4MkvIrgrWipUHc3KUDohIwN2DDpspCI1"></script>
-
- <!-- Global site tag (gtag.js) - Google Analytics -->
- <script async src="https://www.googletagmanager.com/gtag/js?id=UA-128199158-1"></script>
- <script>
- window.dataLayer = window.dataLayer || [];
- function gtag() { dataLayer.push(arguments); }
- gtag('js', new Date());
-
- gtag('config', 'UA-128199158-1');
- </script>
-
- <script>
- function setSelectionRange(input, selectionStart, selectionEnd) {
- if (input.setSelectionRange) {
- input.focus();
- input.setSelectionRange(selectionStart, selectionEnd);
- }
- else if (input.createTextRange) {
- var range = input.createTextRange();
- range.collapse(true);
- range.moveEnd('character', selectionEnd);
- range.moveStart('character', selectionStart);
- range.select();
- }
- }
-
- function setCaretToPos(input, pos) {
- setSelectionRange(input, pos, pos);
- }
-
- $(document).ready(function () {
- // Catch all events related to changes http://stackoverflow.com/questions/21215049/disable-text-entry-in-input-type-number
- $('.number-input').on('change keyup', function () {
- var sanitized = $(this).val().replace(/[^0-9]/g, ''); // Remove invalid characters
- $(this).val(sanitized); // Update value
- });
-
- $(function () {
- var tb = document.getElementById('textBoxSearch');
- if (tb) {
- var val = $("#textBoxSearch").val();
- var caretPos = val.length;
- setCaretToPos(tb, caretPos);
- }
- });
- });
- </script>
-
-
-</body>
-</html>
|
