├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── lib
└── emoji-magic
│ ├── src
│ ├── app_data
│ │ ├── emoji.js
│ │ ├── emoji.test.js
│ │ ├── emoji_data.js
│ │ ├── emoji_data.test.js
│ │ └── emojilib_thesaurus.js
│ ├── app_ui
│ │ ├── emoji_details.css
│ │ ├── emoji_details.css.map
│ │ ├── emoji_details.js
│ │ ├── emoji_details.sass
│ │ ├── emoji_picker.css
│ │ ├── emoji_picker.css.map
│ │ └── emoji_picker.sass
│ ├── bind_to_dom.js
│ ├── browser_action.html
│ ├── emoji.html
│ ├── icons
│ │ ├── crystal-ball-1f52e.svg
│ │ ├── emoji-magic assets.sketch
│ │ │ ├── Data
│ │ │ ├── metadata
│ │ │ └── version
│ │ ├── emoji-magic screenshots.sketch
│ │ │ ├── Data
│ │ │ ├── metadata
│ │ │ └── version
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── icon128.png
│ │ ├── icon16.png
│ │ ├── icon19.png
│ │ ├── icon48.png
│ │ ├── promo-tile-large.png
│ │ ├── promo-tile-marquee.png
│ │ ├── promo-tile-small.png
│ │ ├── promo-tile.png
│ │ ├── purple.png
│ │ └── yellow.png
│ ├── index.html
│ ├── js_utils
│ │ ├── code_points.js
│ │ ├── code_points.test.js
│ │ ├── emulate_node_modules.js
│ │ ├── matchers.js
│ │ ├── matchers.test.js
│ │ └── store.js
│ └── site
│ │ ├── site.css
│ │ ├── site.css.map
│ │ └── site.sass
│ └── third_party
│ └── emojilib
│ ├── LICENSE
│ ├── data-by-emoji.json
│ ├── emoji-en-US.json
│ └── emojilib.js
├── manifest.json
├── package.json
├── rollup.config.js
├── screenshots
├── emoji-magic-blue.png
├── emoji-magic-obsidian-1.gif
└── emoji-magic-obsidian-2.gif
├── src
├── cfg.ts
├── interfaces.ts
├── main.ts
├── search_modal.ts
└── search_modal_suggest_variant.ts
├── styles.css
└── version-bump.mjs
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release Obsidian plugin
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 |
8 | env:
9 | PLUGIN_NAME: obsidian-emoji-magic # Change this to match the id of your plugin.
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Use Node.js
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: "14.x"
21 |
22 | - name: Build
23 | id: build
24 | run: |
25 | npm install
26 | npm run build
27 | mkdir ${{ env.PLUGIN_NAME }}
28 | cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }}
29 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
30 | ls
31 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
32 |
33 | - name: Create Release
34 | id: create_release
35 | uses: actions/create-release@v1
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 | VERSION: ${{ github.ref }}
39 | with:
40 | tag_name: ${{ github.ref }}
41 | release_name: ${{ github.ref }}
42 | draft: false
43 | prerelease: false
44 |
45 | - name: Upload zip file
46 | id: upload-zip
47 | uses: actions/upload-release-asset@v1
48 | env:
49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50 | with:
51 | upload_url: ${{ steps.create_release.outputs.upload_url }}
52 | asset_path: ./${{ env.PLUGIN_NAME }}.zip
53 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
54 | asset_content_type: application/zip
55 |
56 | - name: Upload main.js
57 | id: upload-main
58 | uses: actions/upload-release-asset@v1
59 | env:
60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61 | with:
62 | upload_url: ${{ steps.create_release.outputs.upload_url }}
63 | asset_path: ./main.js
64 | asset_name: main.js
65 | asset_content_type: text/javascript
66 |
67 | - name: Upload manifest.json
68 | id: upload-manifest
69 | uses: actions/upload-release-asset@v1
70 | env:
71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72 | with:
73 | upload_url: ${{ steps.create_release.outputs.upload_url }}
74 | asset_path: ./manifest.json
75 | asset_name: manifest.json
76 | asset_content_type: application/json
77 |
78 | - name: Upload styles.css
79 | id: upload-css
80 | uses: actions/upload-release-asset@v1
81 | env:
82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83 | with:
84 | upload_url: ${{ steps.create_release.outputs.upload_url }}
85 | asset_path: ./styles.css
86 | asset_name: styles.css
87 | asset_content_type: text/css
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Intellij
2 | *.iml
3 | .idea
4 |
5 | # npm
6 | node_modules
7 | package-lock.json
8 |
9 | # build
10 | main.js
11 | *.js.map
12 | data.json
13 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution;
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows
28 | [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/).
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This plugin lets you add emoji to your Obsidian notes more easiy using an enhanced keyword search.
2 |
3 | It has a large dictionary of keywords so you can find emoji you wouldn't find otherwise:
4 |
5 | * broad terms like "car" -> `🚓🚋🚔🏎️🚐🚕🚖`
6 | * colors like "orange" -> `📙🧡🍊🥕🚼`
7 |
8 | Animated Screenshot:
9 |
10 | 
11 |
12 | Still Screenshot:
13 |
14 | 
15 |
16 | ## Features
17 |
18 | 1. Secure -- local only, no internet
19 | 2. Rich keyword search -- the dictionary contains `1,812` emoji with `199,658` searchable keywords and thesaurus entries.
20 | 3. Fast -- just keyboard shortcut and click
21 | 4. Keyboard friendly -- arrow keys, tab, however you want.
22 |
23 | > Note: This is an [Obsidian](https://obsidian.md/)-compatible fork of the [Emoji Magic](https://github.com/SimplGy/emoji-magic) Chrome Extension I wrote.
24 |
25 | ## Installing this Plugin
26 |
27 | 1. Open settings. (if you haven't yet, one time: Third party plugin -> Disable Safe mode)
28 | 1. Click "Browse community plugins" -> Search for "Magic File Hotkey"
29 | 1. Install it, then click "enable"
30 | 1. Add a hotkey. I like `cmd + shift + e` ("e" for "emoji").
31 |
32 | ## Guiding Principles
33 | > Goals and Non-goals
34 |
35 | * Easy find
36 | * an effort is made to include lots of possible matches
37 | * eg: the color "green" or the feeling "happy"
38 | * Keyboard friendly.
39 | * Eg: `keyboard shortcut` -> `search phrase` -> `enter` and done.
40 | * Because this plugin shows a 2d grid of emoji, built two-dimensional arrow key support for navigating.
41 | * Actual emoji. No images.
42 | * This means: no custom emoji
43 | * Also means: will render platform-appropriate versions. The visual you see can vary depending on where you're viewing the file
44 | * Also means: you may see empty rectangles for emoji that are defined, but not supported by your device. (eg: 🦩 "flamingo" won't be there if you're on an older Mac)
45 |
46 | ## Similar Obsidian Plugins
47 |
48 | > AKA: "why did I need to build this?"
49 |
50 | * [Emoji Shortcodes](https://github.com/phibr0/obsidian-emoji-shortcodes)
51 | * Excellent plugin, seems to work great, but I like having a popup search panel instead of using the `:smile:` kind of syntax
52 | * [Emoji Toolbar](https://github.com/oliveryh/obsidian-emoji-toolbar)
53 | * Currently the most popular "emoji" plugin for Obsidian
54 | * Uses images instead of the text emoji char itself. That means some emoji can look different in the picker VS when I actually insert them in my file. There is a setting that might be related, but I wasn't able to get it to work. (Update: appears fixed in [v0.4.0](https://github.com/oliveryh/obsidian-emoji-toolbar/releases/tag/0.4.0))
55 | * Started breaking for me (might be a "live preview" only bug). Would insert emoji at the start of the file instead of where my cursor is. (Update: appears fixed in [v0.4.0](https://github.com/oliveryh/obsidian-emoji-toolbar/releases/tag/0.4.0) or earlier)
56 |
57 |
58 |
59 | ## Developing this Plugin
60 |
61 | ### Building
62 |
63 | ```
64 | # npm install
65 | npm run dev
66 | ```
67 |
68 | (for auto refreshing) install `git clone https://github.com/pjeby/hot-reload.git` and turn it on
69 |
70 | ### (one time) symlink from dev environment to Obsidian plugin dir
71 |
72 | right click on the obsidian plugins folder, "new terminal at folder". then:
73 |
74 | ```
75 | ln -s /Users/eric/Projects/obsidian-emoji-magic obsidian-emoji-magic
76 | ```
77 |
78 | ### (rarely) Sync with the upstream project
79 |
80 | ```
81 | rm -rf lib/emoji-magic
82 | git clone https://github.com/SimplGy/emoji-magic.git lib/emoji-magic
83 | ```
84 |
85 | ### Releasing
86 |
87 | 1. Update the version in `package.json` (only)
88 | 2. `npm run version`
89 |
90 | > This will trigger `.github/workflows/release.yml`.
91 | >
92 | > Verify the workflow is running [here](https://github.com/SimplGy/obsidian-emoji-magic/actions).
93 | > Verify [releases here](https://github.com/SimplGy/obsidian-emoji-magic/releases)
94 |
95 | (you're done) simply doing a github release and running release.yml will make the new version of the plugin available on the Obsidian marketplace. Nice!
96 |
97 |
98 |
99 | ## TODO
100 | PRs welcome.
101 |
102 | - [x] change from 3 -> 2 chars required to see search results, and blank out defaults during inital typing so it doesn't look like there's a bug
103 | - [ ] improve startup time -- 2023-03-18: took a look, but not seeing anything obvious here. I think I have to find a way to defer parsing of the large `emojilib_thesaurus.js` file. AFAIK there is no actual processing happening at startup, it's just JS parse time dragging things out.
104 | - [ ] solve the `zwj` problem (eg: "plane")
105 | - [ ] (upstream) improve some of the ranking (car, plane) and
106 | - [ ] (upstream) fix the lack of "stemming" problem (eg: "race car")
107 | - [ ] (upstream) adapt the headless stuff to be easier to reuse.
108 |
109 | ## Future
110 | - [ ] consider using Obsidian's native [SuggestModal](https://marcus.se.net/obsidian-plugin-docs/reference/typescript/classes/SuggestModal) or [FuzzySuggestModal](https://marcus.se.net/obsidian-plugin-docs/reference/typescript/classes/FuzzySuggestModal) -- https://marcus.se.net/obsidian-plugin-docs/user-interface/modals -- I thought this couldn't work with a grid UI, but there's [prior art here](https://github.com/oliveryh/obsidian-emoji-toolbar/commit/1b8f7624f575cb183271a3d969ee5939c4763f8a)
111 |
112 |
113 |
114 |
115 | ## Contributing
116 |
117 | Contributions welcome. See CONTRIBUTING.md.
118 |
119 | ## Disclaimer
120 |
121 | This is not an officially supported Google product.
122 |
--------------------------------------------------------------------------------
/lib/emoji-magic/src/app_data/emoji.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2019 Google LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | https://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | const {toChar, toObj, array} = require('./emoji_data');
18 | const store = require('../js_utils/store');
19 | const {minPrefixOverlapsByWordSet} = require('../js_utils/matchers');
20 | const {fromCodePoints, toCodePoints} = require('../js_utils/code_points'); // re-exporting these for convenience
21 |
22 |
23 |
24 | module.exports = (() => {
25 | const CLIPBOARD_CLEAR_DELAY = 1000; // how long to leave the psuedo-clipboard content in place before clearing. (this user visible "clipboard" is what enables clicking three times in a row to copy a set of three emoji)
26 | const CLOSE_POPUP_DELAY = 800; // How long to wait after a successful copy before closing the browser extension popup
27 | const ANIMATION = 'tada'; // must match a css class
28 | const TADA_ANIM_DURATION = 400; // should match css animation duration
29 | const SHOW_COPIED_MSG_DURATION = 600; // how long to show the "copied" message (on the button)
30 | const RECENT_KEY = 'recent-selections';
31 | const RESULT_LIMIT = 8 * 15; // for render perf, don't draw everything. 15 rows fit in Chrome's 600px height limit for default font size/zoom settings.
32 | const RECENT_SELECTION_LIMIT = 8 * 1; // at the default font size, there are 8 per row
33 | const DEFAULT_RESULTS = ['🔮','🎩','✨','🐇'];
34 | let recentSelections = []; // in memory store of recent selections. format is plain chars, not objects
35 | store.get(RECENT_KEY, val => recentSelections = val || []); // seed the recentSelections
36 | let willClearClipboard; // reference to the timeout that will clear this
37 |
38 | const $ = {
39 | results: () => document.getElementById('results'),
40 | clipboard: () => document.getElementById('clipboard'),
41 | copyBtn: () => document.getElementById('copyBtn'),
42 | search: () => document.getElementById('search'),
43 | };
44 |
45 | // from David Walsh
46 | function debounce(func, wait, immediate) {
47 | var timeout;
48 | return function() {
49 | var context = this, args = arguments;
50 | var later = function() {
51 | timeout = null;
52 | if (!immediate) func.apply(context, args);
53 | };
54 | var callNow = immediate && !timeout;
55 | clearTimeout(timeout);
56 | timeout = setTimeout(later, wait);
57 | if (callNow) func.apply(context, args);
58 | };
59 | };
60 |
61 | // Given any number of arrays, return the smallest intersection
62 | // No order is guaranteed
63 | // Does not mutate input arrays
64 | function intersection(...arrays) {
65 | return arrays.reduce((acc, cur) =>
66 | acc.filter(p => cur.includes(p))
67 | );
68 | }
69 |
70 | // Given an array of arrays, return a flat array. Depth 1
71 | function flatten(arr) {
72 | return arr.reduce((acc, val) => acc.concat(val), []);
73 | }
74 |
75 |
76 | // add recent selection, but only track the most recent k
77 | // also deduplicates: if it's already present, remove matches first before putting the most recent at the front
78 | function trackRecent(char) {
79 | recentSelections = recentSelections.filter(c => c !== char);
80 | recentSelections.unshift(char);
81 | if (recentSelections.length > RECENT_SELECTION_LIMIT) {
82 | recentSelections = recentSelections.slice(0, RECENT_SELECTION_LIMIT);
83 | }
84 | store.set(RECENT_KEY, recentSelections);
85 | }
86 |
87 | // Main search method.
88 | // Given a search string, return an array of matching emoji objects
89 | // The data is usually pre-bound but you can provide your own data set as well (eg: testing)
90 | // eg: options.useThesaurus is not used today (always match the thesaurus)
91 | const searchOn = (data = []) => (str = '', options) => {
92 | str = str.trim();
93 |
94 | // Blank search? Exit early.
95 | if (str === '') {
96 | const results = recentSelections.length > 0 ? recentSelections : DEFAULT_RESULTS;
97 | return results.map(toObj);
98 | }
99 |
100 | const matchesByStrength = data
101 | .map(r => [
102 | r,
103 | matchStrengthFor(r, str),
104 | ])
105 | // Anything above zero counts as a match (either keywords or thesaurus):
106 | .filter(r => r[1] > 0)
107 | // Sort by the match strength
108 | .sort((a, b) => b[1] - a[1])
109 | // Drop the match strength vector from the response (plain emoji objects):
110 | .map(r => r[0]);
111 |
112 | // TODO: make it obvious which words are matching, so it's not confusing why unrelated-seeming results appear
113 | // console.log(matchesByStrength.map(({char, match}) => char + ' ' + match));
114 |
115 | return matchesByStrength;
116 | };
117 |
118 | function htmlForAllEmoji(emojiArray = []) {
119 | const html = emojiArray.map(htmlForEmoji).join('\n');
120 | return html
121 | }
122 |
123 | function htmlForEmoji(emoji) {
124 | const char = toChar(emoji);
125 | return `
`;
126 | }
127 |
128 | // Take an emoji object and summarize it as a multiline string (useful for tooltips)
129 | function summaryString(emoji) {
130 | const { keywords = [], thesaurus = []} = emoji
131 | return `${emoji.char} ${emoji.name}\n\n` +
132 |
133 | `keywords: ${keywords.join(', ')}\n` +
134 | `version (emoji/unicode): ${emoji.emoji_version} / ${emoji.unicode_version}\n\n` +
135 |
136 | thesaurus
137 | .filter(arr => arr.length > 0)
138 | .map((arr, idx) => `${keywords[idx]}:\n${arr.join(', ')}`).join('\n\n')
139 | }
140 |
141 | // Dom aware
142 | // Tasks: Put result html into container, update disabled state of btn
143 | function render(emoji = []) {
144 | emoji = emoji.slice(0, RESULT_LIMIT);
145 | $.results().innerHTML = htmlForAllEmoji(emoji);
146 | if (emoji.length === 0) {
147 | $.copyBtn().setAttribute('disabled', true);
148 | } else {
149 | $.copyBtn().removeAttribute('disabled');
150 | }
151 | }
152 |
153 | // Dom aware
154 | function onPressEmoji(char) {
155 | $.clipboard().value += char;
156 |
157 | // Every so often, clear this "clipboard"
158 | clearTimeout(willClearClipboard);
159 | willClearClipboard = setTimeout(clearClipboard, CLIPBOARD_CLEAR_DELAY);
160 |
161 | trackRecent(char);
162 | copyToClipboard();
163 | }
164 |
165 | // Dom aware
166 | // true if there are any items in the tray ready to copy
167 | function clipboardHasStuff() {
168 | return $.clipboard().value.trim().length > 0;
169 | }
170 |
171 | function clearClipboard() {
172 | $.clipboard().value = '';
173 | }
174 |
175 | // true if there are no results in dom right now
176 | function noResults() {
177 | return $.results().hasChildNodes()
178 | }
179 |
180 | // Dom aware
181 | function copyToClipboard({instant = false} = {}) {
182 | if (!clipboardHasStuff()) return;
183 |
184 | // Select the text and copy, then return focus to where it was
185 | const focusedEl = document.activeElement; // what used to be focused? make sure to reselect it.
186 | $.clipboard().select();
187 | document.execCommand('copy');
188 | focusedEl.focus();
189 |
190 | // Animate success and close popup
191 | if (instant) {
192 | animateCopySuccess();
193 | closePopup();
194 | } else {
195 | animateCopySuccessDebounced();
196 | setTimeout(closePopup, CLOSE_POPUP_DELAY);
197 | }
198 | }
199 |
200 | const animateCopySuccessDebounced = debounce(animateCopySuccess, 200);
201 |
202 | function animateCopySuccess() {
203 | const $copyBtn = document.getElementById('copyBtn');
204 | $copyBtn.innerText = "Copied";
205 | $copyBtn.classList.add(ANIMATION);
206 | $copyBtn.classList.add('copied');
207 | setTimeout(() => $copyBtn.classList.remove(ANIMATION), TADA_ANIM_DURATION);
208 | setTimeout(() => $copyBtn.innerText = "Copy", SHOW_COPIED_MSG_DURATION);
209 | }
210 |
211 | // Close the popup, but not if:
212 | // 1) we're in a unit test (node)
213 | // 2) We're on the demo/webapp, rather than in the Chrome browser_popup
214 | function closePopup() {
215 | if (typeof window.close === 'function' && chrome.browserAction) {
216 | window.close();
217 | }
218 | }
219 |
220 | function copyFirstEmoji() {
221 | const btn = $.results().querySelector('button'); // get the first button
222 | if (!btn) return;
223 | const char = btn.innerText;
224 | $.clipboard().value = char;
225 | copyToClipboard({instant: true});
226 | }
227 |
228 | // Move focus horizontally. +1 is right, -1 is left.
229 | // wraps around
230 | // boundary protection works because focusOn just exits if it doesn't find an el to focus
231 | // really really dom coupled
232 | function moveFocusX(direction = 0) {
233 | const wasFromSearch = document.activeElement === $.search();
234 | if (wasFromSearch) return; // don't do custom arrow behavior if we're in the search box already
235 | const curLi = document.activeElement.parentElement;
236 | const idx = Array.from($.results().children).indexOf(curLi);
237 | focusOn(idx + direction);
238 | }
239 |
240 | // +1 is down, -1 is up
241 | function moveFocusY(direction = 0) {
242 |
243 | // special case: moving down from search
244 | const wasFromSearch = document.activeElement === $.search();
245 | if(wasFromSearch) {
246 | if (direction > 0) {
247 | focusOn(0);
248 | }
249 | return; // don't do custom arrow behavior if we're in the search box already
250 | }
251 |
252 | let el = document.activeElement.parentElement;
253 | const left = el.offsetLeft;
254 | const top = el.offsetTop;
255 | let prevEl;
256 |
257 | // if we're going down a row...
258 | if (direction > 0) {
259 | // 1. look forward until we find something with a higher offsetTop (first el of next row)
260 | while(el && el.offsetTop <= top) {
261 | prevEl = el;
262 | el = el.nextElementSibling;
263 | }
264 |
265 | // 2. look forward until we find something >= the current offsetLeft (same col, or close)
266 | while (el && el.offsetLeft < left) {
267 | prevEl = el;
268 | el = el.nextElementSibling;
269 | }
270 |
271 | // another approach: just look for the next element with nearly the same left position, it'll be in the next row.
272 | // I like the way the current one handles sparse rows though.
273 |
274 | // if we're going up a row...
275 | } else if (direction < 0) {
276 | // 1. look backward until we find something with a lower offsetTop (last el of previous row)
277 | while(el && el.offsetTop >= top) {
278 | prevEl = el;
279 | el = el.previousElementSibling;
280 | }
281 | // 2. look backward until we find something <= the current offsetLeft
282 | while (el && el.offsetLeft > left) {
283 | prevEl = el;
284 | el = el.previousElementSibling;
285 | }
286 | }
287 |
288 | if (el) {
289 | focusOnButton(el);
290 | } else if(prevEl) {
291 | // if moving up and we didn't find a correct focus target, choose the search box
292 | if (direction < 0) {
293 | $.search().focus();
294 | $.search().select();
295 | } else {
296 | focusOnButton(prevEl);
297 | }
298 | }
299 |
300 | }
301 |
302 | // really really dom coupled
303 | function focusOn(idx) {
304 | const li = $.results().children[idx];
305 | if (!li) return;
306 | focusOnButton(li);
307 | }
308 |
309 | function focusOnButton(el) {
310 | const btn = el.querySelector('button');
311 | if (!btn) return;
312 | btn.focus();
313 | }
314 |
315 | /*
316 | example emoji object:
317 | {
318 | "name": "grinning",
319 | "keywords": [
320 | "face",
321 | "smile",
322 | "happy",
323 | "joy",
324 | ":D",
325 | "grin"
326 | ],
327 | "char": "😀",
328 | "category": "people",
329 | "thesaurus": [
330 | ["human face","external body part"],
331 | ["expression","look"]
332 | ]
333 | }
334 | */
335 |
336 | /*
337 | * For a given emoji object and query, return a numeric "strength" for the match.
338 | * It gets match strength for all the terms on 2 dimensions:
339 | * 1: Keywords
340 | * 2: Thesaurus
341 | *
342 | * It then averages the match strength on each dimension.
343 | * To simplify sorting, it returns the keyword match strength in standard order,
344 | * summed with the thesaurus match strength divided by 1000
345 | * eg: 1 number: `0.501` (indicating a 50% avg keyword match and a 100% avg thesaurus match)
346 | */
347 | function matchStrengthFor(emojiObj = {}, query = '') {
348 | const [n, k, t] = computeMatchVectorForEmoji(emojiObj, query);
349 |
350 | // debug: log the match strength with nice readable output:
351 | // if (n + k + t > 0) {
352 | // console.log(`\nmatchStrengthFor('${emojiObj.char}', '${query}') name`, n)
353 | // console.log(`matchStrengthFor('${emojiObj.char}', '${query}') keyword`, k)
354 | // console.log(`matchStrengthFor('${emojiObj.char}', '${query}') thesaurus`, t)
355 | // }
356 |
357 | return n + k/10 + t/1000; // eg: 1.001 or 0.005
358 | }
359 |
360 | // returns a set like [0, 0, 0.25] representing match strength on three vectors:
361 | // [name, keywords, thesaurus]
362 | function computeMatchVectorForEmoji(emojiObj = {}, query = '') {
363 | const nameParts = emojiObj.nameParts;
364 | const keywords = emojiObj.keywords;
365 | const thesaurus = flatten(emojiObj.thesaurus);
366 | return minPrefixOverlapsByWordSet(query)([nameParts, keywords, thesaurus]);
367 | }
368 |
369 |
370 |
371 | return {
372 | __id__: 'emoji',
373 | // Key data method
374 | search: searchOn(array),
375 | // UI Methods
376 | render,
377 | onPressEmoji,
378 | copyToClipboard,
379 | copyFirstEmoji,
380 | closePopup,
381 | // re-exports
382 | toCodePoints,
383 | fromCodePoints,
384 | // Exported for test only:
385 | computeMatchVectorForEmoji,
386 | matchStrengthFor,
387 | htmlForAllEmoji,
388 | searchOn,
389 |
390 | // New exports:
391 | moveFocusX,
392 | moveFocusY,
393 | htmlForEmoji,
394 | summaryString,
395 | DEFAULT_RESULTS,
396 | RECENT_SELECTION_LIMIT,
397 | toObj,
398 | }
399 | })();
400 |
--------------------------------------------------------------------------------
/lib/emoji-magic/src/app_data/emoji.test.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2019 Google LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | https://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | const {array, toChars, toObj} = require('./emoji_data');
18 | const store = require('../js_utils/store');
19 | const emoji = require('./emoji');
20 |
21 | function flatten(arr) {
22 | return arr.reduce((acc, val) => acc.concat(val), []);
23 | }
24 |
25 |
26 |
27 | describe("emoji.js", () => {
28 |
29 | // shorthand for doing an emoji search and converting the results to chars instead of objects
30 | s = (term) => toChars(emoji.search(term));
31 |
32 | // expect that a term includes emojiChar somewhere in results
33 | expectSearchIncludes = (term, emojiChar) => {
34 | let result = s(term);
35 | expect(result).toContain(emojiChar);
36 | };
37 |
38 | // expect that the top ranked result for a term is emojiChar
39 | expectFirstResult = (term, emojiChar) => {
40 | const result = s(term);
41 | expect(result[0]).toBe(emojiChar);
42 | };
43 |
44 | describe("setup", () => {
45 | it("has prerequisites defined", () => {
46 | expect(store).toBeDefined();
47 | expect(emoji).toBeDefined();
48 | });
49 | });
50 |
51 | describe("emoji.search()", () => {
52 | it('returns an array of structured objects', () => {
53 | const result = emoji.search('diamond');
54 | expect(result.length).toBeGreaterThanOrEqual(6); // might add more emoji. Never remove or invalidate keywords
55 | expect(toChars(result)).toContain('💍');
56 | expect(result[0].nameParts.length).toBeGreaterThan(0);
57 | expect(result[0].keywords.length).toBeGreaterThan(0);
58 | expect(result[0].thesaurus.length).toBeGreaterThan(0);
59 | });
60 | it("matches epected symbols for 'crystal'", () => {
61 | const result = s('crystal');
62 | // WARNING: if you check length on a joined string result instead of this array, you'll probably see 4, not 2, because many emoji are multi-byte chars.
63 | expect(result).toContain('🔮');
64 | expect(result).toContain('💠');
65 | });
66 |
67 | it("matches epected symbols for 'pepper'", () => {
68 | const topThreeResults = s('pepper').slice(0,3);
69 | expect(topThreeResults).toContain('🌶️');
70 | });
71 |
72 | it("matches other simple searches", () => {
73 | expect(s('green')).toContain('💚');
74 | });
75 |
76 | it("handles multi-word searches", () => {
77 | expectFirstResult('blue heart', '💙');
78 | expectFirstResult(' heart blue ', '💙'); // funny spacing
79 | expectFirstResult('green ball', '🎾');
80 | expectFirstResult('sad cat', '😿');
81 | });
82 |
83 | it("only matches prefixes", () => {
84 | const result = s('ice');
85 | expect(result).not.toContain('👨💼'); // Shouldn't match "off[ice] worker"
86 | });
87 |
88 | it("matches some common prefixes", () => {
89 | const result = s('fire');
90 | expect(result).toContain('🔥'); // fire
91 | expect(result).toContain('🚒'); // fire engine
92 | expect(result).toContain('👩🚒'); // woman firefighter
93 | });
94 |
95 | it("can match the second word of multi word emoji names like 'shaved ice'", () => {
96 | const emojiObj = toObj('🍧');
97 | expect(emojiObj.name).toBe('shaved ice');
98 |
99 | // even though it doesn't startWith "ice", because it's in the name, it matches
100 | expectSearchIncludes('ice', '🍧')
101 | });
102 |
103 | it("does't just blindly match anywhere in multi word emoji names", () => {
104 | const emojiObj = toObj('🍧');
105 | expect(emojiObj.name).toBe('shaved ice');
106 |
107 | // even though it doesn't startWith "ice", because it's in the name, it matches
108 | expect(s('have')).not.toContain('🍧') // "have" does not match "shaved"
109 | });
110 | });
111 |
112 | describe("emoji.computeMatchVectorForEmoji(emojiObj, query)", () => {
113 | const subject = toObj('💚');
114 | it('calculates a three component vector', () => {
115 | const result = emoji.computeMatchVectorForEmoji(subject, 'love green heart');
116 | expect(result.length).toBe(3);
117 | });
118 | it('finds "green heart" in keywords for "💚"', () => {
119 | const [n, k, t] = emoji.computeMatchVectorForEmoji(subject, 'green heart');
120 | expect(n).toBe(1); // 100% match on name -- all of the input words match 100% of something in corpus
121 | });
122 | it('finds "love" in "💚"', () => {
123 | const [n, k, t] = emoji.computeMatchVectorForEmoji(subject, 'love');
124 | expect(k).toBeGreaterThan(0, 'keyword match strength');
125 | expect(t).toBeGreaterThan(0, 'thesaurus match strength');
126 | });
127 | it('returns a zero score on "rutabaga" for "💚"', () => {
128 | const [n, k, t] = emoji.computeMatchVectorForEmoji(subject, 'rutabaga');
129 | expect(n + k + t).toBe(0);
130 | });
131 | });
132 |
133 | describe("emoji.matchStrengthFor(emojiObj, query)", () => {
134 | // mock "emoji" data with known match percentages
135 | const subject = {
136 | char: 'z',
137 | nameParts: ['aaaaaaaa', 'cc'],
138 | keywords: ['aaaa', 'cc'],
139 | thesaurus: [
140 | ['aa', 'cccccccc'],
141 | ['aaa', 'ccccc']
142 | ],
143 | };
144 | // Multi word query, so all words must match at least a little bit
145 | const query = 'aa cc';
146 |
147 | it('computeMatchVectorForEmoji() returns expected vector strengths', () => {
148 | const [n, k, t] = emoji.computeMatchVectorForEmoji(subject, query);
149 | expect(n).toBe(0.25, 'name'); // "aa" is the weakest query term at "aa"/"aaaaaaaa" -- 25% match
150 | expect(k).toBe(0.5, 'keyword'); // "aa" is the weakest query term at "aa"/"aaaa" -- 50% match
151 | expect(t).toBe(0.4, 'thesaurus'); // "cc" is the weakest query term at "cc"/"ccccc" -- 40% match
152 | });
153 | it('calculates match strength as expected', () => {
154 | // Test the vector combination "math"
155 | const strength = emoji.matchStrengthFor(subject, query);
156 | const name = 0.25; // known result, validated in test above
157 | const keyword = 0.5; // known result
158 | const thesaurus = 0.4; // known result
159 | expect(strength).toBe(name + keyword/10 + thesaurus/1000); // the keywords and thesaurus matches are downweighted so they sort lower
160 | });
161 | });
162 |
163 | describe("Thesaurus Matching", () => {
164 |
165 | it("finds things using thesaurus it otherwise wouldn't (eg: 😀)", () => {
166 | expectSearchIncludes('visage', '😀');
167 | });
168 |
169 | it("finds things using thesaurus it otherwise wouldn't (eg: 🥶)", () => {
170 | expectSearchIncludes('ice', '🥶');
171 |
172 | });
173 |
174 | it("finds things using thesaurus it otherwise wouldn't (eg: 🤬)", () => {
175 | expectSearchIncludes('tempestuous', '🤬');
176 | });
177 |
178 | it("finds synonyms for 'barf'", () => {
179 | expectSearchIncludes('sick', '🤮'); // this is the human entered, "canonical" keyword
180 | expectSearchIncludes('barf', '🤮');
181 | expectSearchIncludes('puke', '🤮');
182 | });
183 | });
184 |
185 |
186 |
187 | // -------------------------------------------- Reverse search. Do we see expected keywords and synonyms for a symbol?
188 | describe("Emoji Objects", () => {
189 | const sickEmoji = toObj('🤮');
190 | it('has keywords', () => {
191 | expect(sickEmoji.keywords.length).toBeGreaterThanOrEqual(2); // '🤮' has some keywords
192 | });
193 | it('has a reasonable looking thesaurus', () => {
194 | const sickThesaurus = flatten(sickEmoji.thesaurus);
195 | expect(sickThesaurus.length).toBeGreaterThan(80);
196 | // Has expected synonyms
197 | expect(sickThesaurus).toEqual(jasmine.arrayContaining(['afflicted','seasick','dizzy','unwell']));
198 | // And not ones you wouldn't
199 | expect(sickThesaurus).not.toEqual(jasmine.arrayContaining(['giraffe','elephant']));
200 | });
201 | });
202 |
203 | });
204 |
--------------------------------------------------------------------------------
/lib/emoji-magic/src/app_data/emoji_data.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2019 Google LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | https://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | const emojilib_thesaurus = require('./emojilib_thesaurus');
17 |
18 |
19 |
20 | module.exports = ((global) => {
21 |
22 | // Utility functions
23 |
24 | // emoji object to char. O(1)
25 | const toChar = o => o.char;
26 |
27 | // char to emoji object. O(n)
28 | const toObj = char => emojilib_thesaurus.array.find(el => el.char === char);
29 |
30 | // Convert from an array of emoji objects to a plain char
31 | const toChars = (arr = []) => arr.map(toChar);
32 |
33 | return {
34 | toObj,
35 | toChar,
36 | toChars,
37 | array: emojilib_thesaurus.array,
38 | __id__: 'emoji_data', // emulated node modules
39 | };
40 | })(this);
--------------------------------------------------------------------------------
/lib/emoji-magic/src/app_data/emoji_data.test.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2019 Google LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | https://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | const emojiData = require('./emoji_data');
18 |
19 |
20 |
21 | describe("emoji_data.js", () => {
22 | describe("data array", () => {
23 | it("has more than 1500 emojis", () => {
24 | expect(emojiData.array.length).toBeGreaterThan(1500);
25 | });
26 | it("does not always copy the 'name' into 'keywords'", () => {
27 | const o = emojiData.toObj('💚');
28 | expect(o.name).toBe('green heart');
29 | expect(o.keywords).not.toContain('green');
30 | expect(o.keywords).not.toContain('heart');
31 | expect(o.keywords).not.toContain('green heart');
32 | });
33 | it("has the full_slug in 'keywords'", () => {
34 | const o = emojiData.toObj('💚');
35 | expect(o.slug).toBe('green_heart');
36 | expect(o.keywords).toContain('green_heart');
37 | });
38 | it("provides a special location for multi word names", () => {
39 | const o = emojiData.toObj('💚');
40 | expect(o.name).toBe('green heart');
41 | expect(o.nameParts).toContain('green');
42 | expect(o.nameParts).toContain('heart');
43 | });
44 | // Validate this, because then the search weighting score would count it for both
45 | it("never has a name that exactly matches a keyword", () => {
46 | for (let emoji of emojiData.array) {
47 | for (let namePart of emoji.nameParts) {
48 | expect(emoji.keywords).not.toContain(namePart, emoji.char);
49 | }
50 | }
51 | const o = emojiData.toObj('💚');
52 | expect(o.slug).toBe('green_heart');
53 | expect(o.keywords).toContain('green_heart');
54 | });
55 | });
56 | });
--------------------------------------------------------------------------------
/lib/emoji-magic/src/app_ui/emoji_details.css:
--------------------------------------------------------------------------------
1 | .emoji-details dl {
2 | color: gray;
3 | }
4 |
5 | .emoji-details h1, .emoji-details h2 {
6 | text-align: center;
7 | }
8 |
9 | .emoji-details h1 {
10 | font-size: 6rem;
11 | margin-bottom: 0;
12 | }
13 |
14 | .emoji-details h2 {
15 | margin-top: 0;
16 | padding-top: 5px;
17 | border-top: 1px solid rgba(255, 255, 255, 0.3);
18 | }
19 | /*# sourceMappingURL=emoji_details.css.map */
--------------------------------------------------------------------------------
/lib/emoji-magic/src/app_ui/emoji_details.css.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "mappings": "AAAA,AACE,cADY,CACZ,EAAE,CAAC;EACD,KAAK,EAAE,IAAI;CAAG;;AAFlB,AAGE,cAHY,CAGZ,EAAE,EAHJ,cAAc,CAGR,EAAE,CAAC;EACL,UAAU,EAAE,MAAM;CAAG;;AAJzB,AAKE,cALY,CAKZ,EAAE,CAAC;EACD,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,CAAC;CAAG;;AAPvB,AAQE,cARY,CAQZ,EAAE,CAAC;EACD,UAAU,EAAE,CAAC;EACb,WAAW,EAAE,GAAG;EAChB,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,wBAAqB;CAAG",
4 | "sources": [
5 | "emoji_details.sass"
6 | ],
7 | "names": [],
8 | "file": "emoji_details.css"
9 | }
--------------------------------------------------------------------------------
/lib/emoji-magic/src/app_ui/emoji_details.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2019 Google LLC
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | https://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 |
18 |
19 | module.exports = ((global) => {
20 | const WORD = 'word';
21 |
22 | // given an array of words, return html with WORD classnames
23 | const renderWords = (arr, joiner = ", ") => arr.map(renderWord).join(joiner);
24 | // Given a word, return html with WORD classname
25 | const renderWord = str => `${str}`
26 |
27 | // return a string of rendered html for the given emoji
28 | function render(emoji) {
29 | const { keywords = [], thesaurus = [] } = emoji;
30 |
31 | const nameWords = emoji.name.split('_');
32 |
33 | return `
34 |