docsmith 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/.rspec +3 -0
- data/.rspec_status +212 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +66 -0
- data/Rakefile +8 -0
- data/USAGE.md +510 -0
- data/docs/superpowers/plans/2026-04-01-docsmith-full-plan.md +6459 -0
- data/docs/superpowers/plans/2026-04-08-parsers-remove-branches-docs.md +2112 -0
- data/docs/superpowers/specs/2026-04-01-docsmith-phase1-design.md +340 -0
- data/docsmith_spec.md +630 -0
- data/lib/docsmith/auto_save.rb +29 -0
- data/lib/docsmith/comments/anchor.rb +68 -0
- data/lib/docsmith/comments/comment.rb +44 -0
- data/lib/docsmith/comments/manager.rb +73 -0
- data/lib/docsmith/comments/migrator.rb +64 -0
- data/lib/docsmith/configuration.rb +95 -0
- data/lib/docsmith/diff/engine.rb +39 -0
- data/lib/docsmith/diff/parsers/html.rb +64 -0
- data/lib/docsmith/diff/parsers/markdown.rb +60 -0
- data/lib/docsmith/diff/renderers/base.rb +62 -0
- data/lib/docsmith/diff/renderers/registry.rb +41 -0
- data/lib/docsmith/diff/renderers.rb +10 -0
- data/lib/docsmith/diff/result.rb +77 -0
- data/lib/docsmith/diff.rb +6 -0
- data/lib/docsmith/document.rb +44 -0
- data/lib/docsmith/document_version.rb +50 -0
- data/lib/docsmith/errors.rb +18 -0
- data/lib/docsmith/events/event.rb +19 -0
- data/lib/docsmith/events/hook_registry.rb +14 -0
- data/lib/docsmith/events/notifier.rb +22 -0
- data/lib/docsmith/rendering/html_renderer.rb +36 -0
- data/lib/docsmith/rendering/json_renderer.rb +29 -0
- data/lib/docsmith/version.rb +5 -0
- data/lib/docsmith/version_manager.rb +143 -0
- data/lib/docsmith/version_tag.rb +25 -0
- data/lib/docsmith/versionable.rb +252 -0
- data/lib/docsmith.rb +52 -0
- data/lib/generators/docsmith/install/install_generator.rb +27 -0
- data/lib/generators/docsmith/install/templates/create_docsmith_tables.rb.erb +64 -0
- data/lib/generators/docsmith/install/templates/docsmith_initializer.rb.erb +19 -0
- data/sig/docsmith.rbs +4 -0
- metadata +196 -0
data/USAGE.md
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
# Docsmith Usage Guide
|
|
2
|
+
|
|
3
|
+
Docsmith adds snapshot-based versioning, format-aware diffs, and inline comments to any
|
|
4
|
+
ActiveRecord model. It stores all data in your existing database — no external services.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
1. [Installation](#1-installation)
|
|
11
|
+
2. [Setup — Migration](#2-setup--migration)
|
|
12
|
+
3. [Setup — Include Versionable](#3-setup--include-versionable)
|
|
13
|
+
4. [Per-Class Configuration](#4-per-class-configuration)
|
|
14
|
+
5. [Global Configuration](#5-global-configuration)
|
|
15
|
+
6. [Saving Versions](#6-saving-versions)
|
|
16
|
+
7. [Auto-Save and Debounce](#7-auto-save-and-debounce)
|
|
17
|
+
8. [Querying Versions](#8-querying-versions)
|
|
18
|
+
9. [Restoring Versions](#9-restoring-versions)
|
|
19
|
+
10. [Tagging Versions](#10-tagging-versions)
|
|
20
|
+
11. [Diffs](#11-diffs)
|
|
21
|
+
12. [Comments](#12-comments)
|
|
22
|
+
13. [Events and Hooks](#13-events-and-hooks)
|
|
23
|
+
14. [Standalone Document API](#14-standalone-document-api)
|
|
24
|
+
15. [Configuration Reference](#15-configuration-reference)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Installation
|
|
29
|
+
|
|
30
|
+
Add to your `Gemfile`:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
gem "docsmith"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bundle install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. Setup — Migration
|
|
45
|
+
|
|
46
|
+
Run the install generator to create the migration:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
rails generate docsmith:install
|
|
50
|
+
rails db:migrate
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This creates four tables:
|
|
54
|
+
|
|
55
|
+
| Table | Purpose |
|
|
56
|
+
|-------------------------|----------------------------------------------|
|
|
57
|
+
| `docsmith_documents` | One record per versioned model instance |
|
|
58
|
+
| `docsmith_versions` | Content snapshots (immutable) |
|
|
59
|
+
| `docsmith_version_tags` | Named tags on specific versions |
|
|
60
|
+
| `docsmith_comments` | Inline and document-level comments |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 3. Setup — Include Versionable
|
|
65
|
+
|
|
66
|
+
Add `include Docsmith::Versionable` to any ActiveRecord model. Optionally configure
|
|
67
|
+
it with `docsmith_config`:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
class Article < ApplicationRecord
|
|
71
|
+
include Docsmith::Versionable
|
|
72
|
+
|
|
73
|
+
docsmith_config do
|
|
74
|
+
content_field :body # which column holds the document content
|
|
75
|
+
content_type :markdown # :markdown, :html, or :json
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
That is all you need. Docsmith automatically creates a shadow `Docsmith::Document`
|
|
81
|
+
record the first time a version is saved for each model instance.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 4. Per-Class Configuration
|
|
86
|
+
|
|
87
|
+
`docsmith_config` accepts a block that can set any of the following keys:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
class LegalDocument < ApplicationRecord
|
|
91
|
+
include Docsmith::Versionable
|
|
92
|
+
|
|
93
|
+
docsmith_config do
|
|
94
|
+
content_field :body # column to snapshot (default: :body)
|
|
95
|
+
content_type :html # :markdown (default), :html, :json
|
|
96
|
+
auto_save false # disable auto-save callback (default: true)
|
|
97
|
+
debounce 60 # seconds between auto-saves (default: 30)
|
|
98
|
+
max_versions 50 # cap on stored versions per document (default: nil = unlimited)
|
|
99
|
+
content_extractor ->(r) { r.body.to_s.strip } # override content_field with a proc
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**`content_extractor`** is useful when the field you want to version is not a plain
|
|
105
|
+
string column:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
docsmith_config do
|
|
109
|
+
content_field :body_data # ActiveStorage attachment or JSONB column
|
|
110
|
+
content_type :markdown
|
|
111
|
+
content_extractor ->(record) { record.body_data.to_plain_text }
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 5. Global Configuration
|
|
118
|
+
|
|
119
|
+
Set defaults for the whole app in `config/initializers/docsmith.rb`:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
Docsmith.configure do |config|
|
|
123
|
+
config.default_content_field = :body
|
|
124
|
+
config.default_content_type = :markdown
|
|
125
|
+
config.auto_save = true
|
|
126
|
+
config.default_debounce = 30 # seconds
|
|
127
|
+
config.max_versions = nil # nil = unlimited
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Resolution order: **per-class `docsmith_config`** > **global `Docsmith.configure`** > **gem defaults**.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 6. Saving Versions
|
|
136
|
+
|
|
137
|
+
Call `save_version!` to take an explicit snapshot:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
article = Article.find(1)
|
|
141
|
+
article.body = "Updated content here."
|
|
142
|
+
article.save!
|
|
143
|
+
|
|
144
|
+
version = article.save_version!(author: current_user, summary: "Fixed typo in intro")
|
|
145
|
+
# => #<Docsmith::DocumentVersion version_number: 3, content_type: "markdown", ...>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- Returns the new `DocumentVersion` record.
|
|
149
|
+
- Returns `nil` if the content has not changed since the last snapshot.
|
|
150
|
+
- Raises `Docsmith::InvalidContentField` if `content_field` returns a non-String and
|
|
151
|
+
no `content_extractor` is configured.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 7. Auto-Save and Debounce
|
|
156
|
+
|
|
157
|
+
When `auto_save: true` (the default), Docsmith hooks into ActiveRecord's `after_save`
|
|
158
|
+
callback and automatically takes a snapshot after every model save — subject to the
|
|
159
|
+
debounce window.
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
article.body = "New draft"
|
|
163
|
+
article.save! # triggers auto_save_version! internally
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The **debounce** prevents a snapshot from being created if another snapshot was already
|
|
167
|
+
taken within the last N seconds (default: 30). This avoids flooding the version history
|
|
168
|
+
when a user is rapidly typing and saving.
|
|
169
|
+
|
|
170
|
+
You can also call `auto_save_version!` directly:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
article.auto_save_version!(author: current_user)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
To disable auto-save for a class:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
docsmith_config { auto_save false }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 8. Querying Versions
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
# All versions, ordered by version_number ascending
|
|
188
|
+
article.versions
|
|
189
|
+
# => ActiveRecord::Relation<Docsmith::DocumentVersion>
|
|
190
|
+
|
|
191
|
+
# Latest version
|
|
192
|
+
article.current_version
|
|
193
|
+
# => #<Docsmith::DocumentVersion version_number: 5, ...>
|
|
194
|
+
|
|
195
|
+
# Specific version by number
|
|
196
|
+
article.version(3)
|
|
197
|
+
# => #<Docsmith::DocumentVersion version_number: 3, ...>
|
|
198
|
+
|
|
199
|
+
# Inspect content
|
|
200
|
+
article.version(2).content # => "Body text at v2"
|
|
201
|
+
article.version(2).content_type # => "markdown"
|
|
202
|
+
article.version(2).author # => #<User id: 1, ...>
|
|
203
|
+
article.version(2).change_summary # => "Second draft"
|
|
204
|
+
article.version(2).created_at # => 2026-03-01 14:22:00 UTC
|
|
205
|
+
|
|
206
|
+
# Render a version's content
|
|
207
|
+
article.version(2).render(:html) # => "<p>Body text at v2</p>"
|
|
208
|
+
article.version(2).render(:json) # => '{"version":2,"content":"..."}'
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 9. Restoring Versions
|
|
214
|
+
|
|
215
|
+
Restore creates a **new version** whose content matches an older snapshot. It never
|
|
216
|
+
mutates existing version records.
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
restored = article.restore_version!(2, author: current_user)
|
|
220
|
+
# => #<Docsmith::DocumentVersion version_number: 6, change_summary: "Restored from v2", ...>
|
|
221
|
+
|
|
222
|
+
article.reload.body # => the body content from v2
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- The model's `content_field` column is updated via `update_column` (bypasses callbacks
|
|
226
|
+
to avoid a duplicate auto-save).
|
|
227
|
+
- Fires the `:version_restored` event hook (see §13).
|
|
228
|
+
- Raises `Docsmith::VersionNotFound` if the version number does not exist.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 10. Tagging Versions
|
|
233
|
+
|
|
234
|
+
Tags are named pointers to specific versions, unique per document.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# Create a tag
|
|
238
|
+
article.tag_version!(3, name: "v1.0-release", author: current_user)
|
|
239
|
+
|
|
240
|
+
# Look up a version by tag name
|
|
241
|
+
v = article.tagged_version("v1.0-release")
|
|
242
|
+
# => #<Docsmith::DocumentVersion version_number: 3, ...>
|
|
243
|
+
|
|
244
|
+
# List tag names on a version
|
|
245
|
+
article.version_tags(3)
|
|
246
|
+
# => ["v1.0-release", "stable"]
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- Raises `Docsmith::TagAlreadyExists` if the name is already used on this document.
|
|
250
|
+
- Raises `Docsmith::VersionNotFound` if the version number does not exist.
|
|
251
|
+
|
|
252
|
+
**Interaction with `max_versions`:** Tagged versions are never pruned automatically.
|
|
253
|
+
If all versions are tagged and a prune would be needed, `Docsmith::MaxVersionsExceeded`
|
|
254
|
+
is raised.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## 11. Diffs
|
|
259
|
+
|
|
260
|
+
Docsmith computes diffs between any two versions. The parser used depends on the
|
|
261
|
+
document's `content_type`.
|
|
262
|
+
|
|
263
|
+
### Diff from version N to current
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
result = article.diff_from(1)
|
|
267
|
+
# => #<Docsmith::Diff::Result from_version: 1, to_version: 5, ...>
|
|
268
|
+
|
|
269
|
+
result.additions # => integer count of added tokens
|
|
270
|
+
result.deletions # => integer count of removed tokens
|
|
271
|
+
result.to_html # => HTML string with <ins>/<del> markup
|
|
272
|
+
result.to_json # => JSON string with stats and changes array
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Diff between two named versions
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
result = article.diff_between(2, 4)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Format-aware parsers
|
|
282
|
+
|
|
283
|
+
| `content_type` | Parser | Token unit |
|
|
284
|
+
|----------------|--------|-----------|
|
|
285
|
+
| `markdown` | `Docsmith::Diff::Parsers::Markdown` | Each whitespace-delimited word; newline runs are one token |
|
|
286
|
+
| `html` | `Docsmith::Diff::Parsers::Html` | Each HTML tag (including attributes) is one token; words in text are separate tokens |
|
|
287
|
+
| `json` | `Docsmith::Diff::Renderers::Base` | Line-level (whole lines) |
|
|
288
|
+
|
|
289
|
+
**Markdown example:**
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
# v1 content: "The quick brown fox"
|
|
293
|
+
# v2 content: "The quick red fox"
|
|
294
|
+
result = article.diff_between(1, 2)
|
|
295
|
+
result.changes
|
|
296
|
+
# => [{ type: :modification, line: 3, old_content: "brown", new_content: "red" }]
|
|
297
|
+
result.additions # => 0
|
|
298
|
+
result.deletions # => 0
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**HTML example:**
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
# v1 content: "<p>Hello world</p>"
|
|
305
|
+
# v2 content: "<p>Hello world</p><p>New paragraph</p>"
|
|
306
|
+
# old tokens: ["<p>", "Hello", "world", "</p>"]
|
|
307
|
+
# new tokens: ["<p>", "Hello", "world", "</p>", "<p>", "New", "paragraph", "</p>"]
|
|
308
|
+
# LCS: first 4 tokens match exactly → 4 additions: "<p>", "New", "paragraph", "</p>"
|
|
309
|
+
result = article.diff_between(1, 2)
|
|
310
|
+
result.additions # => 4
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### to_html output
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
result.to_html
|
|
317
|
+
# => '<div class="docsmith-diff">
|
|
318
|
+
# <ins class="docsmith-addition">Ruby</ins>
|
|
319
|
+
# <del class="docsmith-deletion">Python</del>
|
|
320
|
+
# </div>'
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### to_json output
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
JSON.parse(result.to_json)
|
|
327
|
+
# => {
|
|
328
|
+
# "content_type" => "markdown",
|
|
329
|
+
# "from_version" => 1,
|
|
330
|
+
# "to_version" => 3,
|
|
331
|
+
# "stats" => { "additions" => 2, "deletions" => 1 },
|
|
332
|
+
# "changes" => [
|
|
333
|
+
# { "type" => "addition", "position" => { "line" => 5 }, "content" => "Ruby" },
|
|
334
|
+
# { "type" => "deletion", "position" => { "line" => 3 }, "content" => "Python" },
|
|
335
|
+
# { "type" => "modification", "position" => { "line" => 7 }, "old_content" => "foo", "new_content" => "bar" }
|
|
336
|
+
# ]
|
|
337
|
+
# }
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## 12. Comments
|
|
343
|
+
|
|
344
|
+
Comments can be attached to a specific version. They are either **document-level** (no
|
|
345
|
+
position) or **range-anchored** (tied to a character offset range).
|
|
346
|
+
|
|
347
|
+
### Add a comment
|
|
348
|
+
|
|
349
|
+
```ruby
|
|
350
|
+
# Document-level comment
|
|
351
|
+
comment = article.add_comment!(
|
|
352
|
+
version: 2,
|
|
353
|
+
body: "This section needs a citation.",
|
|
354
|
+
author: current_user
|
|
355
|
+
)
|
|
356
|
+
comment.anchor_type # => "document"
|
|
357
|
+
|
|
358
|
+
# Range-anchored (inline) comment — offsets into the version's content string
|
|
359
|
+
comment = article.add_comment!(
|
|
360
|
+
version: 2,
|
|
361
|
+
body: "Cite this claim.",
|
|
362
|
+
author: current_user,
|
|
363
|
+
anchor: { start_offset: 42, end_offset: 78 }
|
|
364
|
+
)
|
|
365
|
+
comment.anchor_type # => "range"
|
|
366
|
+
comment.anchor_data["start_offset"] # => 42
|
|
367
|
+
comment.anchor_data["anchored_text"] # => the substring from offset 42–78
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Thread replies
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
reply = article.add_comment!(
|
|
374
|
+
version: 2,
|
|
375
|
+
body: "Good point, fixing now.",
|
|
376
|
+
author: other_user,
|
|
377
|
+
parent: comment
|
|
378
|
+
)
|
|
379
|
+
comment.replies # => [reply]
|
|
380
|
+
reply.parent # => comment
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Query comments
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
# All comments across all versions (AR relation)
|
|
387
|
+
article.comments
|
|
388
|
+
|
|
389
|
+
# Comments on a specific version
|
|
390
|
+
article.comments_on(version: 2)
|
|
391
|
+
|
|
392
|
+
# Filter by type
|
|
393
|
+
article.comments_on(version: 2, type: :range) # inline only
|
|
394
|
+
article.comments_on(version: 2, type: :document) # document-level only
|
|
395
|
+
|
|
396
|
+
# Unresolved comments across all versions
|
|
397
|
+
article.unresolved_comments
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Resolve a comment
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
Docsmith::Comments::Manager.resolve!(comment, by: current_user)
|
|
404
|
+
comment.reload.resolved # => true
|
|
405
|
+
comment.resolved_by # => current_user
|
|
406
|
+
comment.resolved_at # => Time
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Migrate comments between versions
|
|
410
|
+
|
|
411
|
+
When a new version is saved, document-level comments from the previous version can be
|
|
412
|
+
migrated forward:
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
article.migrate_comments!(from: 2, to: 3)
|
|
416
|
+
# Copies document-level (non-range) comments from v2 to v3.
|
|
417
|
+
# Range comments are not migrated — their offsets may no longer be valid.
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## 13. Events and Hooks
|
|
423
|
+
|
|
424
|
+
Docsmith fires synchronous events you can subscribe to via `Docsmith.configure`.
|
|
425
|
+
|
|
426
|
+
```ruby
|
|
427
|
+
Docsmith.configure do |config|
|
|
428
|
+
config.on(:version_created) do |event|
|
|
429
|
+
Rails.logger.info "[Docsmith] v#{event.version.version_number} saved on #{event.document.title}"
|
|
430
|
+
AuditLog.create!(action: "version_created", record: event.record)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
config.on(:version_restored) do |event|
|
|
434
|
+
Rails.logger.info "[Docsmith] Restored to v#{event.version.version_number}"
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
config.on(:version_tagged) do |event|
|
|
438
|
+
Rails.logger.info "[Docsmith] Tagged v#{event.version.version_number} as '#{event.tag_name}'"
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Event payload** (`event` is a `Docsmith::Events::Event`):
|
|
444
|
+
|
|
445
|
+
| Field | Type | Always present |
|
|
446
|
+
|----------------|--------------------------------------|----------------|
|
|
447
|
+
| `event.record` | The originating AR model (or Document if standalone) | yes |
|
|
448
|
+
| `event.document` | `Docsmith::Document` | yes |
|
|
449
|
+
| `event.version` | `Docsmith::DocumentVersion` | yes |
|
|
450
|
+
| `event.author` | whatever you passed as `author:` | yes |
|
|
451
|
+
| `event.tag_name` | String (`:version_tagged` only) | no |
|
|
452
|
+
| `event.from_version` | DocumentVersion (`:version_restored` only) | no |
|
|
453
|
+
|
|
454
|
+
Hooks fire before `ActiveSupport::Notifications` so they are synchronous and blocking.
|
|
455
|
+
Keep hooks fast.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## 14. Standalone Document API
|
|
460
|
+
|
|
461
|
+
`Docsmith::Versionable` is a convenience wrapper. You can use `Docsmith::Document` and
|
|
462
|
+
`Docsmith::VersionManager` directly without any model mixin:
|
|
463
|
+
|
|
464
|
+
```ruby
|
|
465
|
+
doc = Docsmith::Document.create!(
|
|
466
|
+
title: "My API Spec",
|
|
467
|
+
content: "# Version 1\n\nInitial spec.",
|
|
468
|
+
content_type: "markdown"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
v1 = Docsmith::VersionManager.save!(doc, author: nil, summary: "Initial draft")
|
|
472
|
+
doc.update_column(:content, "# Version 1\n\nRevised spec.")
|
|
473
|
+
v2 = Docsmith::VersionManager.save!(doc, author: nil, summary: "Revised intro")
|
|
474
|
+
|
|
475
|
+
# Diff
|
|
476
|
+
result = Docsmith::Diff.between(v1, v2)
|
|
477
|
+
result.additions # => number of added tokens
|
|
478
|
+
result.to_html # => HTML diff markup
|
|
479
|
+
|
|
480
|
+
# Restore
|
|
481
|
+
Docsmith::VersionManager.restore!(doc, version: 1, author: nil)
|
|
482
|
+
doc.reload.content # => "# Version 1\n\nInitial spec."
|
|
483
|
+
|
|
484
|
+
# Tag
|
|
485
|
+
Docsmith::VersionManager.tag!(doc, version: 1, name: "golden", author: nil)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## 15. Configuration Reference
|
|
491
|
+
|
|
492
|
+
| Key | Default | Description |
|
|
493
|
+
|-----|---------|-------------|
|
|
494
|
+
| `default_content_field` | `:body` | Column to snapshot when no per-class override |
|
|
495
|
+
| `default_content_type` | `:markdown` | Content type for new documents |
|
|
496
|
+
| `auto_save` | `true` | Enable after_save auto-snapshot |
|
|
497
|
+
| `default_debounce` | `30` | Seconds between auto-saves |
|
|
498
|
+
| `max_versions` | `nil` | Max snapshots per document; `nil` = unlimited |
|
|
499
|
+
| `content_extractor` | `nil` | Global proc overriding `content_field` |
|
|
500
|
+
| `table_prefix` | `"docsmith"` | Table name prefix |
|
|
501
|
+
| `diff_context_lines` | `3` | Context lines in diff output |
|
|
502
|
+
|
|
503
|
+
**Error classes:**
|
|
504
|
+
|
|
505
|
+
| Class | Raised when |
|
|
506
|
+
|-------|-------------|
|
|
507
|
+
| `Docsmith::InvalidContentField` | `content_field` returns a non-String |
|
|
508
|
+
| `Docsmith::VersionNotFound` | Requested version number does not exist |
|
|
509
|
+
| `Docsmith::TagAlreadyExists` | Tag name already used on this document |
|
|
510
|
+
| `Docsmith::MaxVersionsExceeded` | All versions are tagged and pruning is blocked |
|