auto_glossary 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +35 -0
- data/Rakefile +4 -0
- data/app/assets/stylesheets/glossary.css +180 -0
- data/app/controllers/auto_glossary/glossary_controller.rb +34 -0
- data/app/helpers/auto_glossary/glossary_helper.rb +70 -0
- data/app/javascript/controllers/glossary_controller.js +199 -0
- data/app/services/auto_glossary/wikipedia_glossary_service.rb +143 -0
- data/config/routes.rb +6 -0
- data/lib/auto_glossary/engine.rb +33 -0
- data/lib/auto_glossary/version.rb +5 -0
- data/lib/auto_glossary.rb +8 -0
- data/lib/generators/auto_glossary/install/install_generator.rb +47 -0
- data/lib/generators/auto_glossary/install/templates/glossary_controller.js +199 -0
- data/sig/auto_glossary.rbs +4 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fddd93fba68c2b99a3ea1e2017393b053f0341ec1ee68815bd90afaf23166ea0
|
|
4
|
+
data.tar.gz: 4bb4aeb3e1752b1cc3965229586785ca91b49ccbe994b3db665fd42fd40c074d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ece9e23d884a593aa881e15be743d2eb9293dac1b1ab3576410036c10b3704c903d42fb9cde3d751ddd198162aeb7e93a0b2f4bac8d6794d1a73616b809de31f
|
|
7
|
+
data.tar.gz: c9bcd7372a7216cb60309e2a6e658619bf6472d2f49e8450a2283214963a5c2a82d5bc64a69df736eb034c69fd8c47e632c8190189ab22ba012726dddcd02a69
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Will Johnston
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# AutoGlossary
|
|
2
|
+
|
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
|
4
|
+
|
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/auto_glossary`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
|
10
|
+
|
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
TODO: Write usage instructions here
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
30
|
+
|
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
32
|
+
|
|
33
|
+
## Contributing
|
|
34
|
+
|
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/auto_glossary.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* Glossary term styling */
|
|
2
|
+
.glossary-term {
|
|
3
|
+
text-decoration: underline dotted;
|
|
4
|
+
text-decoration-color: rgba(0, 100, 200, 0.8);
|
|
5
|
+
text-decoration-thickness: 2px;
|
|
6
|
+
text-underline-offset: 3px;
|
|
7
|
+
cursor: help;
|
|
8
|
+
transition: text-decoration-color 0.2s ease;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.glossary-term:hover {
|
|
12
|
+
text-decoration-color: rgba(0, 100, 200, 1);
|
|
13
|
+
text-decoration-thickness: 2.5px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.glossary-term:focus {
|
|
17
|
+
outline: 2px solid rgba(0, 100, 200, 0.5);
|
|
18
|
+
outline-offset: 2px;
|
|
19
|
+
border-radius: 2px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Tooltip styling */
|
|
23
|
+
.glossary-tooltip {
|
|
24
|
+
position: absolute;
|
|
25
|
+
z-index: 10000;
|
|
26
|
+
background: #2c3e50;
|
|
27
|
+
color: #fff;
|
|
28
|
+
padding: 8px 12px;
|
|
29
|
+
border-radius: 4px;
|
|
30
|
+
font-size: 0.9rem;
|
|
31
|
+
line-height: 1.4;
|
|
32
|
+
max-width: 300px;
|
|
33
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
|
34
|
+
pointer-events: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.glossary-tooltip::before {
|
|
38
|
+
content: '';
|
|
39
|
+
position: absolute;
|
|
40
|
+
bottom: 100%;
|
|
41
|
+
left: 20px;
|
|
42
|
+
border: 6px solid transparent;
|
|
43
|
+
border-bottom-color: #2c3e50;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.glossary-tooltip em {
|
|
47
|
+
color: #95a5a6;
|
|
48
|
+
font-style: italic;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Modal styling */
|
|
52
|
+
.glossary-modal {
|
|
53
|
+
display: none;
|
|
54
|
+
position: fixed;
|
|
55
|
+
z-index: 9999;
|
|
56
|
+
left: 0;
|
|
57
|
+
top: 0;
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: 100%;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.glossary-modal.active {
|
|
63
|
+
display: block;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.glossary-modal-backdrop {
|
|
67
|
+
position: fixed;
|
|
68
|
+
top: 0;
|
|
69
|
+
left: 0;
|
|
70
|
+
width: 100%;
|
|
71
|
+
height: 100%;
|
|
72
|
+
background: rgba(0, 0, 0, 0.5);
|
|
73
|
+
backdrop-filter: blur(2px);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.glossary-modal-dialog {
|
|
77
|
+
position: relative;
|
|
78
|
+
margin: 50px auto;
|
|
79
|
+
max-width: 600px;
|
|
80
|
+
background: #fff;
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
|
83
|
+
animation: slideDown 0.3s ease;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@keyframes slideDown {
|
|
87
|
+
from {
|
|
88
|
+
opacity: 0;
|
|
89
|
+
transform: translateY(-50px);
|
|
90
|
+
}
|
|
91
|
+
to {
|
|
92
|
+
opacity: 1;
|
|
93
|
+
transform: translateY(0);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.glossary-modal-header {
|
|
98
|
+
display: flex;
|
|
99
|
+
justify-content: space-between;
|
|
100
|
+
align-items: center;
|
|
101
|
+
padding: 20px;
|
|
102
|
+
border-bottom: 1px solid #e0e0e0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.glossary-modal-title {
|
|
106
|
+
margin: 0;
|
|
107
|
+
font-size: 1.5rem;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
color: #2c3e50;
|
|
110
|
+
text-transform: capitalize;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.glossary-modal-close {
|
|
114
|
+
background: none;
|
|
115
|
+
border: none;
|
|
116
|
+
font-size: 2rem;
|
|
117
|
+
line-height: 1;
|
|
118
|
+
color: #95a5a6;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
padding: 0;
|
|
121
|
+
width: 30px;
|
|
122
|
+
height: 30px;
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
justify-content: center;
|
|
126
|
+
border-radius: 4px;
|
|
127
|
+
transition: all 0.2s ease;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.glossary-modal-close:hover {
|
|
131
|
+
background: #ecf0f1;
|
|
132
|
+
color: #2c3e50;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.glossary-modal-body {
|
|
136
|
+
padding: 20px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.glossary-modal-content {
|
|
140
|
+
font-size: 1rem;
|
|
141
|
+
line-height: 1.6;
|
|
142
|
+
color: #34495e;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.glossary-modal-content p {
|
|
146
|
+
margin: 0 0 15px 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.glossary-modal-content p:last-child {
|
|
150
|
+
margin-bottom: 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.glossary-attribution {
|
|
154
|
+
margin-top: 20px;
|
|
155
|
+
padding-top: 15px;
|
|
156
|
+
border-top: 1px solid #e0e0e0;
|
|
157
|
+
color: #7f8c8d;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.glossary-attribution a {
|
|
161
|
+
color: #3498db;
|
|
162
|
+
text-decoration: none;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.glossary-attribution a:hover {
|
|
166
|
+
text-decoration: underline;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Responsive adjustments */
|
|
170
|
+
@media (max-width: 768px) {
|
|
171
|
+
.glossary-modal-dialog {
|
|
172
|
+
margin: 20px;
|
|
173
|
+
max-width: calc(100% - 40px);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.glossary-tooltip {
|
|
177
|
+
max-width: 250px;
|
|
178
|
+
font-size: 0.85rem;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AutoGlossary
|
|
4
|
+
class GlossaryController < ApplicationController
|
|
5
|
+
skip_before_action :verify_authenticity_token, only: [:definition]
|
|
6
|
+
|
|
7
|
+
# GET /glossary/definition?term=basidiospore
|
|
8
|
+
def definition
|
|
9
|
+
term = params[:term]
|
|
10
|
+
|
|
11
|
+
if term.blank?
|
|
12
|
+
render json: { error: "Term parameter is required" }, status: :bad_request
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
definition = AutoGlossary::WikipediaGlossaryService.get_definition(term)
|
|
17
|
+
|
|
18
|
+
if definition
|
|
19
|
+
render json: { term: term, definition: definition }
|
|
20
|
+
else
|
|
21
|
+
render json: { error: "Definition not found", term: term }, status: :not_found
|
|
22
|
+
end
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
Rails.logger.error "Error fetching glossary definition: #{e.message}"
|
|
25
|
+
render json: { error: "Internal server error" }, status: :internal_server_error
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# GET /glossary - Optional: browse all terms
|
|
29
|
+
def index
|
|
30
|
+
@terms = AutoGlossary::WikipediaGlossaryService.fetch_glossary_terms
|
|
31
|
+
@terms = @terms.sort_by { |k, _v| k.downcase }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AutoGlossary
|
|
4
|
+
module GlossaryHelper
|
|
5
|
+
# Mark glossary terms in text with special HTML markup
|
|
6
|
+
# Options:
|
|
7
|
+
# first_only: true - only mark first occurrence of each term (default: true)
|
|
8
|
+
def mark_glossary_terms(text, options = {})
|
|
9
|
+
return text if text.blank?
|
|
10
|
+
|
|
11
|
+
first_only = options.fetch(:first_only, true)
|
|
12
|
+
marked_terms = Set.new
|
|
13
|
+
|
|
14
|
+
terms = AutoGlossary::WikipediaGlossaryService.fetch_glossary_terms
|
|
15
|
+
return text if terms.empty?
|
|
16
|
+
|
|
17
|
+
# Sort terms by length (longest first) to avoid partial matches
|
|
18
|
+
sorted_terms = terms.keys.sort_by { |t| -t.length }
|
|
19
|
+
|
|
20
|
+
result = text.dup
|
|
21
|
+
|
|
22
|
+
sorted_terms.each do |term|
|
|
23
|
+
# Skip if we've already marked this term and first_only is true
|
|
24
|
+
next if first_only && marked_terms.include?(term.downcase)
|
|
25
|
+
|
|
26
|
+
# Create regex pattern for word boundaries
|
|
27
|
+
# Match the term with optional 's' for plural
|
|
28
|
+
pattern = /\b(#{Regexp.escape(term)}s?)\b/i
|
|
29
|
+
|
|
30
|
+
# Replace first or all occurrences
|
|
31
|
+
if first_only
|
|
32
|
+
result = result.sub(pattern) do |match|
|
|
33
|
+
marked_terms.add(term.downcase)
|
|
34
|
+
glossary_term_tag(match)
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
result = result.gsub(pattern) do |match|
|
|
38
|
+
glossary_term_tag(match)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
result.html_safe
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def glossary_term_tag(term)
|
|
49
|
+
# Normalize to singular form for lookup
|
|
50
|
+
normalized = normalize_term_for_lookup(term)
|
|
51
|
+
%(<span class="glossary-term" data-term="#{CGI.escapeHTML(normalized)}" tabindex="0">#{CGI.escapeHTML(term)}</span>)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def normalize_term_for_lookup(term)
|
|
55
|
+
normalized = term.downcase.strip
|
|
56
|
+
|
|
57
|
+
# Remove common plural endings
|
|
58
|
+
if normalized.end_with?("s") && !normalized.end_with?("ss")
|
|
59
|
+
# Try removing 's' for simple plurals
|
|
60
|
+
singular = normalized.chomp("s")
|
|
61
|
+
|
|
62
|
+
# Check if singular form exists in glossary
|
|
63
|
+
terms = AutoGlossary::WikipediaGlossaryService.fetch_glossary_terms
|
|
64
|
+
return singular if terms.keys.any? { |k| k.downcase == singular }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
normalized
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["modal", "modalContent", "tooltip"]
|
|
5
|
+
static values = {
|
|
6
|
+
url: { type: String, default: "/glossary/definition" }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this.tooltipElement = null
|
|
11
|
+
this.currentTerm = null
|
|
12
|
+
this.definitionCache = new Map()
|
|
13
|
+
|
|
14
|
+
// Add event listeners to all glossary terms
|
|
15
|
+
this.attachGlossaryListeners()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
disconnect() {
|
|
19
|
+
this.removeTooltip()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
attachGlossaryListeners() {
|
|
23
|
+
document.querySelectorAll('.glossary-term').forEach(term => {
|
|
24
|
+
term.addEventListener('mouseenter', this.showTooltip.bind(this))
|
|
25
|
+
term.addEventListener('mouseleave', this.hideTooltip.bind(this))
|
|
26
|
+
term.addEventListener('click', this.showModal.bind(this))
|
|
27
|
+
term.addEventListener('keydown', this.handleKeydown.bind(this))
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
showTooltip(event) {
|
|
32
|
+
const term = event.target.dataset.term
|
|
33
|
+
if (!term) return
|
|
34
|
+
|
|
35
|
+
this.currentTerm = term
|
|
36
|
+
|
|
37
|
+
// Show loading tooltip
|
|
38
|
+
this.createTooltip(event.target, 'Loading...')
|
|
39
|
+
|
|
40
|
+
// Fetch definition
|
|
41
|
+
this.fetchDefinition(term).then(definition => {
|
|
42
|
+
if (this.currentTerm === term) {
|
|
43
|
+
this.updateTooltip(this.truncateDefinition(definition, 150))
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
hideTooltip(event) {
|
|
49
|
+
// Small delay to allow moving to modal
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
if (!this.modalOpen) {
|
|
52
|
+
this.removeTooltip()
|
|
53
|
+
}
|
|
54
|
+
}, 100)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
createTooltip(element, content) {
|
|
58
|
+
this.removeTooltip()
|
|
59
|
+
|
|
60
|
+
this.tooltipElement = document.createElement('div')
|
|
61
|
+
this.tooltipElement.className = 'glossary-tooltip'
|
|
62
|
+
this.tooltipElement.innerHTML = content
|
|
63
|
+
document.body.appendChild(this.tooltipElement)
|
|
64
|
+
|
|
65
|
+
this.positionTooltip(element)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
updateTooltip(content) {
|
|
69
|
+
if (this.tooltipElement) {
|
|
70
|
+
this.tooltipElement.innerHTML = content
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
removeTooltip() {
|
|
75
|
+
if (this.tooltipElement) {
|
|
76
|
+
this.tooltipElement.remove()
|
|
77
|
+
this.tooltipElement = null
|
|
78
|
+
}
|
|
79
|
+
this.currentTerm = null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
positionTooltip(element) {
|
|
83
|
+
if (!this.tooltipElement) return
|
|
84
|
+
|
|
85
|
+
const rect = element.getBoundingClientRect()
|
|
86
|
+
const tooltipRect = this.tooltipElement.getBoundingClientRect()
|
|
87
|
+
|
|
88
|
+
let top = rect.bottom + window.scrollY + 5
|
|
89
|
+
let left = rect.left + window.scrollX
|
|
90
|
+
|
|
91
|
+
// Adjust if tooltip goes off screen
|
|
92
|
+
if (left + tooltipRect.width > window.innerWidth) {
|
|
93
|
+
left = window.innerWidth - tooltipRect.width - 10
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (top + tooltipRect.height > window.innerHeight + window.scrollY) {
|
|
97
|
+
top = rect.top + window.scrollY - tooltipRect.height - 5
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.tooltipElement.style.top = `${top}px`
|
|
101
|
+
this.tooltipElement.style.left = `${left}px`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
showModal(event) {
|
|
105
|
+
event.preventDefault()
|
|
106
|
+
const term = event.target.dataset.term
|
|
107
|
+
if (!term) return
|
|
108
|
+
|
|
109
|
+
this.modalOpen = true
|
|
110
|
+
this.removeTooltip()
|
|
111
|
+
|
|
112
|
+
// Create modal if it doesn't exist
|
|
113
|
+
if (!this.hasModalTarget) {
|
|
114
|
+
this.createModal()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.modalTarget.querySelector('.glossary-modal-content').innerHTML = '<p>Loading...</p>'
|
|
118
|
+
this.modalTarget.classList.add('active')
|
|
119
|
+
this.modalTarget.querySelector('.glossary-modal-title').textContent = term
|
|
120
|
+
|
|
121
|
+
this.fetchDefinition(term).then(definition => {
|
|
122
|
+
const content = this.modalTarget.querySelector('.glossary-modal-content')
|
|
123
|
+
content.innerHTML = `
|
|
124
|
+
<p>${definition}</p>
|
|
125
|
+
<p class="glossary-attribution">
|
|
126
|
+
<small>Source: <a href="https://en.wikipedia.org/wiki/Glossary_of_mycology" target="_blank" rel="noopener">Wikipedia Glossary of Mycology</a> (CC BY-SA 4.0)</small>
|
|
127
|
+
</p>
|
|
128
|
+
`
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
hideModal() {
|
|
133
|
+
if (this.hasModalTarget) {
|
|
134
|
+
this.modalTarget.classList.remove('active')
|
|
135
|
+
}
|
|
136
|
+
this.modalOpen = false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
createModal() {
|
|
140
|
+
const modal = document.createElement('div')
|
|
141
|
+
modal.className = 'glossary-modal'
|
|
142
|
+
modal.dataset.glossaryTarget = 'modal'
|
|
143
|
+
modal.innerHTML = `
|
|
144
|
+
<div class="glossary-modal-backdrop" data-action="click->glossary#hideModal"></div>
|
|
145
|
+
<div class="glossary-modal-dialog">
|
|
146
|
+
<div class="glossary-modal-header">
|
|
147
|
+
<h3 class="glossary-modal-title"></h3>
|
|
148
|
+
<button type="button" class="glossary-modal-close" data-action="click->glossary#hideModal">×</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="glossary-modal-body">
|
|
151
|
+
<div class="glossary-modal-content"></div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
`
|
|
155
|
+
document.body.appendChild(modal)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
handleKeydown(event) {
|
|
159
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
160
|
+
event.preventDefault()
|
|
161
|
+
this.showModal(event)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async fetchDefinition(term) {
|
|
166
|
+
// Check cache first
|
|
167
|
+
if (this.definitionCache.has(term)) {
|
|
168
|
+
return this.definitionCache.get(term)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch(`${this.urlValue}?term=${encodeURIComponent(term)}`, {
|
|
173
|
+
headers: {
|
|
174
|
+
'Accept': 'application/json'
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
throw new Error('Failed to fetch definition')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const data = await response.json()
|
|
183
|
+
const definition = data.definition || 'Definition not found.'
|
|
184
|
+
|
|
185
|
+
// Cache the definition
|
|
186
|
+
this.definitionCache.set(term, definition)
|
|
187
|
+
|
|
188
|
+
return definition
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error('Error fetching glossary definition:', error)
|
|
191
|
+
return 'Unable to load definition.'
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
truncateDefinition(text, maxLength) {
|
|
196
|
+
if (text.length <= maxLength) return text
|
|
197
|
+
return text.substring(0, maxLength) + '... <em>(click for more)</em>'
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module AutoGlossary
|
|
7
|
+
class WikipediaGlossaryService
|
|
8
|
+
GLOSSARY_PAGE = "Glossary_of_mycology"
|
|
9
|
+
API_ENDPOINT = "https://en.wikipedia.org/w/api.php"
|
|
10
|
+
CACHE_EXPIRY = 24.hours
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Fetch all glossary terms from Wikipedia
|
|
14
|
+
def fetch_glossary_terms
|
|
15
|
+
Rails.cache.fetch("wikipedia_glossary_terms", expires_in: CACHE_EXPIRY) do
|
|
16
|
+
parse_glossary_page
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get definition for a specific term
|
|
21
|
+
def get_definition(term)
|
|
22
|
+
normalized_term = normalize_term(term)
|
|
23
|
+
terms = fetch_glossary_terms
|
|
24
|
+
|
|
25
|
+
# Try exact match first
|
|
26
|
+
definition = terms[normalized_term]
|
|
27
|
+
return definition if definition
|
|
28
|
+
|
|
29
|
+
# Try case-insensitive match
|
|
30
|
+
terms.each do |key, value|
|
|
31
|
+
return value if key.downcase == normalized_term.downcase
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Try removing plural 's' if no match found
|
|
35
|
+
if normalized_term.end_with?("s") && !normalized_term.end_with?("ss")
|
|
36
|
+
singular = normalized_term.chomp("s")
|
|
37
|
+
definition = terms[singular]
|
|
38
|
+
return definition if definition
|
|
39
|
+
|
|
40
|
+
# Try case-insensitive singular match
|
|
41
|
+
terms.each do |key, value|
|
|
42
|
+
return value if key.downcase == singular.downcase
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Try removing 'es' ending (e.g., "hyphes" -> "hypha")
|
|
47
|
+
if normalized_term.end_with?("es")
|
|
48
|
+
singular = normalized_term.chomp("es")
|
|
49
|
+
definition = terms[singular]
|
|
50
|
+
return definition if definition
|
|
51
|
+
|
|
52
|
+
terms.each do |key, value|
|
|
53
|
+
return value if key.downcase == singular.downcase
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if a term exists in the glossary
|
|
61
|
+
def term_exists?(term)
|
|
62
|
+
normalized_term = normalize_term(term)
|
|
63
|
+
terms = fetch_glossary_terms
|
|
64
|
+
|
|
65
|
+
terms.key?(normalized_term) || terms.keys.any? { |k| k.downcase == normalized_term.downcase }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def normalize_term(term)
|
|
71
|
+
# Remove hyphens, normalize spacing
|
|
72
|
+
term.to_s.strip.gsub(/[‐‑‒–—―]/, "-")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_glossary_page
|
|
76
|
+
content = fetch_page_content
|
|
77
|
+
return {} unless content
|
|
78
|
+
|
|
79
|
+
extract_terms_from_html(content)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def fetch_page_content
|
|
83
|
+
uri = URI(API_ENDPOINT)
|
|
84
|
+
params = {
|
|
85
|
+
action: "parse",
|
|
86
|
+
page: GLOSSARY_PAGE,
|
|
87
|
+
format: "json",
|
|
88
|
+
prop: "text",
|
|
89
|
+
disableeditsection: 1,
|
|
90
|
+
disabletoc: 1
|
|
91
|
+
}
|
|
92
|
+
uri.query = URI.encode_www_form(params)
|
|
93
|
+
|
|
94
|
+
request = Net::HTTP::Get.new(uri)
|
|
95
|
+
request["User-Agent"] = "AutoGlossaryGem/1.0 (Educational/Research)"
|
|
96
|
+
|
|
97
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 10) do |http|
|
|
98
|
+
http.request(request)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if response.code == "200"
|
|
102
|
+
data = JSON.parse(response.body)
|
|
103
|
+
data.dig("parse", "text", "*")
|
|
104
|
+
else
|
|
105
|
+
Rails.logger.error "Wikipedia API error: #{response.code} - #{response.body}"
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
Rails.logger.error "Error fetching Wikipedia glossary: #{e.message}"
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def extract_terms_from_html(html)
|
|
114
|
+
terms = {}
|
|
115
|
+
|
|
116
|
+
# Wikipedia glossary format: <dt>term</dt><dd>definition</dd>
|
|
117
|
+
html.scan(/<dt[^>]*>(.*?)<\/dt>\s*<dd[^>]*>(.*?)<\/dd>/m) do |term_html, def_html|
|
|
118
|
+
term = strip_html_tags(term_html).strip
|
|
119
|
+
definition = clean_definition(def_html)
|
|
120
|
+
|
|
121
|
+
terms[term] = definition unless term.empty? || definition.empty?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
terms
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def strip_html_tags(text)
|
|
128
|
+
text.gsub(/<\/?[^>]*>/, "")
|
|
129
|
+
.gsub(/\s+/, " ")
|
|
130
|
+
.strip
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def clean_definition(html)
|
|
134
|
+
# Remove edit links, references, etc.
|
|
135
|
+
cleaned = html.gsub(/<span class="mw-editsection".*?<\/span>/m, "")
|
|
136
|
+
.gsub(/<sup[^>]*>.*?<\/sup>/m, "")
|
|
137
|
+
.gsub(/\[edit\]/i, "")
|
|
138
|
+
|
|
139
|
+
strip_html_tags(cleaned).strip
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
|
|
5
|
+
module AutoGlossary
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace AutoGlossary
|
|
8
|
+
|
|
9
|
+
# Make helpers available to the host app
|
|
10
|
+
initializer "auto_glossary.helpers" do
|
|
11
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
12
|
+
helper AutoGlossary::GlossaryHelper
|
|
13
|
+
end
|
|
14
|
+
ActiveSupport.on_load(:action_view) do
|
|
15
|
+
include AutoGlossary::GlossaryHelper
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Add assets paths
|
|
20
|
+
initializer "auto_glossary.assets" do |app|
|
|
21
|
+
if app.config.respond_to?(:assets)
|
|
22
|
+
app.config.assets.paths << root.join("app/assets/stylesheets")
|
|
23
|
+
app.config.assets.precompile += %w[glossary.css]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add JavaScript paths for Stimulus
|
|
28
|
+
initializer "auto_glossary.stimulus" do |app|
|
|
29
|
+
# The JavaScript controller will need to be manually imported by the host app
|
|
30
|
+
# or included via importmap/npm
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module AutoGlossary
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Installs Auto-Glossary into your Rails application"
|
|
11
|
+
|
|
12
|
+
def add_routes
|
|
13
|
+
route <<~RUBY
|
|
14
|
+
# Auto-Glossary routes
|
|
15
|
+
mount AutoGlossary::Engine => "/glossary"
|
|
16
|
+
RUBY
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def copy_javascript
|
|
20
|
+
template "glossary_controller.js", "app/javascript/controllers/glossary_controller.js"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def show_usage
|
|
24
|
+
say "\n"
|
|
25
|
+
say "================================================================", :green
|
|
26
|
+
say "Auto-Glossary has been installed!", :green
|
|
27
|
+
say "================================================================", :green
|
|
28
|
+
say "\n"
|
|
29
|
+
say "Next steps:", :yellow
|
|
30
|
+
say "\n"
|
|
31
|
+
say "1. Add the stylesheet to your layout (app/views/layouts/application.html.erb):", :yellow
|
|
32
|
+
say " <%= stylesheet_link_tag 'glossary', 'data-turbo-track': 'reload' %>", :cyan
|
|
33
|
+
say "\n"
|
|
34
|
+
say "2. Add the Stimulus controller to your <body> tag:", :yellow
|
|
35
|
+
say " <body data-controller=\"glossary\">", :cyan
|
|
36
|
+
say "\n"
|
|
37
|
+
say "3. Use in your views:", :yellow
|
|
38
|
+
say " <%= mark_glossary_terms(@article.body) %>", :cyan
|
|
39
|
+
say "\n"
|
|
40
|
+
say "4. Restart your Rails server", :yellow
|
|
41
|
+
say "\n"
|
|
42
|
+
say "For more info: https://github.com/mrdbidwill/auto-glossary", :blue
|
|
43
|
+
say "\n"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["modal", "modalContent", "tooltip"]
|
|
5
|
+
static values = {
|
|
6
|
+
url: { type: String, default: "/glossary/definition" }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this.tooltipElement = null
|
|
11
|
+
this.currentTerm = null
|
|
12
|
+
this.definitionCache = new Map()
|
|
13
|
+
|
|
14
|
+
// Add event listeners to all glossary terms
|
|
15
|
+
this.attachGlossaryListeners()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
disconnect() {
|
|
19
|
+
this.removeTooltip()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
attachGlossaryListeners() {
|
|
23
|
+
document.querySelectorAll('.glossary-term').forEach(term => {
|
|
24
|
+
term.addEventListener('mouseenter', this.showTooltip.bind(this))
|
|
25
|
+
term.addEventListener('mouseleave', this.hideTooltip.bind(this))
|
|
26
|
+
term.addEventListener('click', this.showModal.bind(this))
|
|
27
|
+
term.addEventListener('keydown', this.handleKeydown.bind(this))
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
showTooltip(event) {
|
|
32
|
+
const term = event.target.dataset.term
|
|
33
|
+
if (!term) return
|
|
34
|
+
|
|
35
|
+
this.currentTerm = term
|
|
36
|
+
|
|
37
|
+
// Show loading tooltip
|
|
38
|
+
this.createTooltip(event.target, 'Loading...')
|
|
39
|
+
|
|
40
|
+
// Fetch definition
|
|
41
|
+
this.fetchDefinition(term).then(definition => {
|
|
42
|
+
if (this.currentTerm === term) {
|
|
43
|
+
this.updateTooltip(this.truncateDefinition(definition, 150))
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
hideTooltip(event) {
|
|
49
|
+
// Small delay to allow moving to modal
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
if (!this.modalOpen) {
|
|
52
|
+
this.removeTooltip()
|
|
53
|
+
}
|
|
54
|
+
}, 100)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
createTooltip(element, content) {
|
|
58
|
+
this.removeTooltip()
|
|
59
|
+
|
|
60
|
+
this.tooltipElement = document.createElement('div')
|
|
61
|
+
this.tooltipElement.className = 'glossary-tooltip'
|
|
62
|
+
this.tooltipElement.innerHTML = content
|
|
63
|
+
document.body.appendChild(this.tooltipElement)
|
|
64
|
+
|
|
65
|
+
this.positionTooltip(element)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
updateTooltip(content) {
|
|
69
|
+
if (this.tooltipElement) {
|
|
70
|
+
this.tooltipElement.innerHTML = content
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
removeTooltip() {
|
|
75
|
+
if (this.tooltipElement) {
|
|
76
|
+
this.tooltipElement.remove()
|
|
77
|
+
this.tooltipElement = null
|
|
78
|
+
}
|
|
79
|
+
this.currentTerm = null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
positionTooltip(element) {
|
|
83
|
+
if (!this.tooltipElement) return
|
|
84
|
+
|
|
85
|
+
const rect = element.getBoundingClientRect()
|
|
86
|
+
const tooltipRect = this.tooltipElement.getBoundingClientRect()
|
|
87
|
+
|
|
88
|
+
let top = rect.bottom + window.scrollY + 5
|
|
89
|
+
let left = rect.left + window.scrollX
|
|
90
|
+
|
|
91
|
+
// Adjust if tooltip goes off screen
|
|
92
|
+
if (left + tooltipRect.width > window.innerWidth) {
|
|
93
|
+
left = window.innerWidth - tooltipRect.width - 10
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (top + tooltipRect.height > window.innerHeight + window.scrollY) {
|
|
97
|
+
top = rect.top + window.scrollY - tooltipRect.height - 5
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.tooltipElement.style.top = `${top}px`
|
|
101
|
+
this.tooltipElement.style.left = `${left}px`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
showModal(event) {
|
|
105
|
+
event.preventDefault()
|
|
106
|
+
const term = event.target.dataset.term
|
|
107
|
+
if (!term) return
|
|
108
|
+
|
|
109
|
+
this.modalOpen = true
|
|
110
|
+
this.removeTooltip()
|
|
111
|
+
|
|
112
|
+
// Create modal if it doesn't exist
|
|
113
|
+
if (!this.hasModalTarget) {
|
|
114
|
+
this.createModal()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.modalTarget.querySelector('.glossary-modal-content').innerHTML = '<p>Loading...</p>'
|
|
118
|
+
this.modalTarget.classList.add('active')
|
|
119
|
+
this.modalTarget.querySelector('.glossary-modal-title').textContent = term
|
|
120
|
+
|
|
121
|
+
this.fetchDefinition(term).then(definition => {
|
|
122
|
+
const content = this.modalTarget.querySelector('.glossary-modal-content')
|
|
123
|
+
content.innerHTML = `
|
|
124
|
+
<p>${definition}</p>
|
|
125
|
+
<p class="glossary-attribution">
|
|
126
|
+
<small>Source: <a href="https://en.wikipedia.org/wiki/Glossary_of_mycology" target="_blank" rel="noopener">Wikipedia Glossary of Mycology</a> (CC BY-SA 4.0)</small>
|
|
127
|
+
</p>
|
|
128
|
+
`
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
hideModal() {
|
|
133
|
+
if (this.hasModalTarget) {
|
|
134
|
+
this.modalTarget.classList.remove('active')
|
|
135
|
+
}
|
|
136
|
+
this.modalOpen = false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
createModal() {
|
|
140
|
+
const modal = document.createElement('div')
|
|
141
|
+
modal.className = 'glossary-modal'
|
|
142
|
+
modal.dataset.glossaryTarget = 'modal'
|
|
143
|
+
modal.innerHTML = `
|
|
144
|
+
<div class="glossary-modal-backdrop" data-action="click->glossary#hideModal"></div>
|
|
145
|
+
<div class="glossary-modal-dialog">
|
|
146
|
+
<div class="glossary-modal-header">
|
|
147
|
+
<h3 class="glossary-modal-title"></h3>
|
|
148
|
+
<button type="button" class="glossary-modal-close" data-action="click->glossary#hideModal">×</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="glossary-modal-body">
|
|
151
|
+
<div class="glossary-modal-content"></div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
`
|
|
155
|
+
document.body.appendChild(modal)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
handleKeydown(event) {
|
|
159
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
160
|
+
event.preventDefault()
|
|
161
|
+
this.showModal(event)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async fetchDefinition(term) {
|
|
166
|
+
// Check cache first
|
|
167
|
+
if (this.definitionCache.has(term)) {
|
|
168
|
+
return this.definitionCache.get(term)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch(`${this.urlValue}?term=${encodeURIComponent(term)}`, {
|
|
173
|
+
headers: {
|
|
174
|
+
'Accept': 'application/json'
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
throw new Error('Failed to fetch definition')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const data = await response.json()
|
|
183
|
+
const definition = data.definition || 'Definition not found.'
|
|
184
|
+
|
|
185
|
+
// Cache the definition
|
|
186
|
+
this.definitionCache.set(term, definition)
|
|
187
|
+
|
|
188
|
+
return definition
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error('Error fetching glossary definition:', error)
|
|
191
|
+
return 'Unable to load definition.'
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
truncateDefinition(text, maxLength) {
|
|
196
|
+
if (text.length <= maxLength) return text
|
|
197
|
+
return text.substring(0, maxLength) + '... <em>(click for more)</em>'
|
|
198
|
+
}
|
|
199
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: auto_glossary
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Will Johnston
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
description: Auto-Glossary provides hover tooltips and click-through definitions for
|
|
41
|
+
technical terms by fetching content from Wikipedia glossaries. Perfect for educational,
|
|
42
|
+
scientific, and technical documentation sites.
|
|
43
|
+
email:
|
|
44
|
+
- mrdbidwill@gmail.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- MIT-LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- Rakefile
|
|
52
|
+
- app/assets/stylesheets/glossary.css
|
|
53
|
+
- app/controllers/auto_glossary/glossary_controller.rb
|
|
54
|
+
- app/helpers/auto_glossary/glossary_helper.rb
|
|
55
|
+
- app/javascript/controllers/glossary_controller.js
|
|
56
|
+
- app/services/auto_glossary/wikipedia_glossary_service.rb
|
|
57
|
+
- config/routes.rb
|
|
58
|
+
- lib/auto_glossary.rb
|
|
59
|
+
- lib/auto_glossary/engine.rb
|
|
60
|
+
- lib/auto_glossary/version.rb
|
|
61
|
+
- lib/generators/auto_glossary/install/install_generator.rb
|
|
62
|
+
- lib/generators/auto_glossary/install/templates/glossary_controller.js
|
|
63
|
+
- sig/auto_glossary.rbs
|
|
64
|
+
homepage: https://auto-glossary.com
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata:
|
|
68
|
+
allowed_push_host: https://rubygems.org
|
|
69
|
+
homepage_uri: https://auto-glossary.com
|
|
70
|
+
source_code_uri: https://github.com/mrdbidwill/auto-glossary
|
|
71
|
+
changelog_uri: https://github.com/mrdbidwill/auto-glossary/blob/main/CHANGELOG.md
|
|
72
|
+
rdoc_options: []
|
|
73
|
+
require_paths:
|
|
74
|
+
- lib
|
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: 3.2.0
|
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '0'
|
|
85
|
+
requirements: []
|
|
86
|
+
rubygems_version: 3.7.1
|
|
87
|
+
specification_version: 4
|
|
88
|
+
summary: Automatically highlight and define technical terms from Wikipedia glossaries
|
|
89
|
+
in Rails applications
|
|
90
|
+
test_files: []
|