inkpen 0.7.1
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/.DS_Store +0 -0
- data/.rubocop.yml +8 -0
- data/.yardopts +11 -0
- data/CLAUDE.md +141 -0
- data/README.md +409 -0
- data/Rakefile +19 -0
- data/app/assets/javascripts/inkpen/controllers/editor_controller.js +2050 -0
- data/app/assets/javascripts/inkpen/controllers/sticky_toolbar_controller.js +667 -0
- data/app/assets/javascripts/inkpen/controllers/toolbar_controller.js +693 -0
- data/app/assets/javascripts/inkpen/export/html.js +637 -0
- data/app/assets/javascripts/inkpen/export/index.js +30 -0
- data/app/assets/javascripts/inkpen/export/markdown.js +697 -0
- data/app/assets/javascripts/inkpen/export/pdf.js +372 -0
- data/app/assets/javascripts/inkpen/extensions/advanced_table.js +640 -0
- data/app/assets/javascripts/inkpen/extensions/block_commands.js +300 -0
- data/app/assets/javascripts/inkpen/extensions/block_gutter.js +338 -0
- data/app/assets/javascripts/inkpen/extensions/callout.js +303 -0
- data/app/assets/javascripts/inkpen/extensions/columns.js +403 -0
- data/app/assets/javascripts/inkpen/extensions/database.js +990 -0
- data/app/assets/javascripts/inkpen/extensions/document_section.js +352 -0
- data/app/assets/javascripts/inkpen/extensions/drag_handle.js +407 -0
- data/app/assets/javascripts/inkpen/extensions/embed.js +629 -0
- data/app/assets/javascripts/inkpen/extensions/enhanced_image.js +566 -0
- data/app/assets/javascripts/inkpen/extensions/export_commands.js +271 -0
- data/app/assets/javascripts/inkpen/extensions/file_attachment.js +593 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/index.js +58 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table.js +638 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_cell.js +100 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_header.js +100 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_constants.js +152 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_helpers.js +254 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_menu.js +282 -0
- data/app/assets/javascripts/inkpen/extensions/preformatted.js +239 -0
- data/app/assets/javascripts/inkpen/extensions/section.js +281 -0
- data/app/assets/javascripts/inkpen/extensions/section_title.js +126 -0
- data/app/assets/javascripts/inkpen/extensions/slash_commands.js +439 -0
- data/app/assets/javascripts/inkpen/extensions/table_of_contents.js +474 -0
- data/app/assets/javascripts/inkpen/extensions/toggle_block.js +332 -0
- data/app/assets/javascripts/inkpen/index.js +87 -0
- data/app/assets/stylesheets/inkpen/advanced_table.css +514 -0
- data/app/assets/stylesheets/inkpen/animations.css +626 -0
- data/app/assets/stylesheets/inkpen/block_gutter.css +265 -0
- data/app/assets/stylesheets/inkpen/callout.css +359 -0
- data/app/assets/stylesheets/inkpen/columns.css +314 -0
- data/app/assets/stylesheets/inkpen/database.css +658 -0
- data/app/assets/stylesheets/inkpen/document_section.css +305 -0
- data/app/assets/stylesheets/inkpen/drag_drop.css +220 -0
- data/app/assets/stylesheets/inkpen/editor.css +652 -0
- data/app/assets/stylesheets/inkpen/embed.css +468 -0
- data/app/assets/stylesheets/inkpen/enhanced_image.css +453 -0
- data/app/assets/stylesheets/inkpen/export.css +499 -0
- data/app/assets/stylesheets/inkpen/file_attachment.css +347 -0
- data/app/assets/stylesheets/inkpen/footnotes.css +136 -0
- data/app/assets/stylesheets/inkpen/inkpen_table.css +608 -0
- data/app/assets/stylesheets/inkpen/preformatted.css +215 -0
- data/app/assets/stylesheets/inkpen/search_replace.css +58 -0
- data/app/assets/stylesheets/inkpen/section.css +236 -0
- data/app/assets/stylesheets/inkpen/slash_menu.css +252 -0
- data/app/assets/stylesheets/inkpen/sticky_toolbar.css +314 -0
- data/app/assets/stylesheets/inkpen/toc.css +386 -0
- data/app/assets/stylesheets/inkpen/toggle.css +260 -0
- data/app/helpers/inkpen/editor_helper.rb +114 -0
- data/app/views/inkpen/_editor.html.erb +139 -0
- data/config/importmap.rb +170 -0
- data/docs/.DS_Store +0 -0
- data/docs/CHANGELOG.md +571 -0
- data/docs/FEATURES.md +436 -0
- data/docs/ROADMAP.md +3029 -0
- data/docs/VISION.md +235 -0
- data/docs/extensions/INKPEN_TABLE.md +482 -0
- data/docs/thinking/CORRECTED_NO_VUE.md +756 -0
- data/docs/thinking/EXECUTIVE_SUMMARY.md +403 -0
- data/docs/thinking/INKPEN_CODE_SAMPLES.md +1479 -0
- data/docs/thinking/INKPEN_MASTER_GUIDE.md +891 -0
- data/docs/thinking/README_START_HERE.md +341 -0
- data/lib/inkpen/configuration.rb +175 -0
- data/lib/inkpen/editor.rb +204 -0
- data/lib/inkpen/engine.rb +32 -0
- data/lib/inkpen/extensions/base.rb +109 -0
- data/lib/inkpen/extensions/code_block_syntax.rb +177 -0
- data/lib/inkpen/extensions/document_section.rb +111 -0
- data/lib/inkpen/extensions/forced_document.rb +183 -0
- data/lib/inkpen/extensions/mention.rb +155 -0
- data/lib/inkpen/extensions/preformatted.rb +111 -0
- data/lib/inkpen/extensions/section.rb +139 -0
- data/lib/inkpen/extensions/slash_commands.rb +100 -0
- data/lib/inkpen/extensions/table.rb +182 -0
- data/lib/inkpen/extensions/task_list.rb +145 -0
- data/lib/inkpen/sticky_toolbar.rb +157 -0
- data/lib/inkpen/toolbar.rb +145 -0
- data/lib/inkpen/version.rb +5 -0
- data/lib/inkpen.rb +101 -0
- data/sig/inkpen.rbs +4 -0
- metadata +165 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inkpen
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace Inkpen
|
|
6
|
+
|
|
7
|
+
initializer "inkpen.assets" do |app|
|
|
8
|
+
# Add stylesheets to asset pipeline
|
|
9
|
+
app.config.assets.paths << root.join("app/assets/stylesheets")
|
|
10
|
+
app.config.assets.paths << root.join("app/assets/javascripts")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer "inkpen.importmap", before: "importmap" do |app|
|
|
14
|
+
# Hook into importmap if available
|
|
15
|
+
if app.config.respond_to?(:importmap)
|
|
16
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
17
|
+
app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
initializer "inkpen.helpers" do |app|
|
|
22
|
+
app.config.to_prepare do
|
|
23
|
+
ActionView::Base.include Inkpen::EditorHelper
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Allow precompilation of inkpen assets
|
|
28
|
+
initializer "inkpen.precompile" do |app|
|
|
29
|
+
app.config.assets.precompile += %w[inkpen/editor.css]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inkpen
|
|
4
|
+
module Extensions
|
|
5
|
+
##
|
|
6
|
+
# Base class for all Inkpen TipTap extensions.
|
|
7
|
+
#
|
|
8
|
+
# Extensions follow the TipTap extension pattern and are configured
|
|
9
|
+
# through this Ruby PORO layer before being passed to the JavaScript
|
|
10
|
+
# TipTap editor.
|
|
11
|
+
#
|
|
12
|
+
# @abstract Subclass and override {#name} and {#to_config} to implement
|
|
13
|
+
# a custom extension.
|
|
14
|
+
#
|
|
15
|
+
# @example Creating a custom extension
|
|
16
|
+
# class MyExtension < Inkpen::Extensions::Base
|
|
17
|
+
# def name
|
|
18
|
+
# :my_extension
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# def to_config
|
|
22
|
+
# { custom_option: true }
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @author Inkpen Team
|
|
27
|
+
# @since 0.2.0
|
|
28
|
+
#
|
|
29
|
+
class Base
|
|
30
|
+
##
|
|
31
|
+
# @return [Hash] the options passed during initialization
|
|
32
|
+
attr_reader :options
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# Initialize a new extension with optional configuration.
|
|
36
|
+
#
|
|
37
|
+
# @param options [Hash] configuration options for the extension
|
|
38
|
+
# @option options [Boolean] :enabled whether the extension is enabled (default: true)
|
|
39
|
+
#
|
|
40
|
+
def initialize(**options)
|
|
41
|
+
@options = default_options.merge(options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# The unique name of the extension.
|
|
46
|
+
#
|
|
47
|
+
# @abstract Subclasses must override this method.
|
|
48
|
+
# @return [Symbol] the extension name
|
|
49
|
+
# @raise [NotImplementedError] if not overridden in subclass
|
|
50
|
+
#
|
|
51
|
+
def name
|
|
52
|
+
raise NotImplementedError, "#{self.class} must implement #name"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# Whether this extension is currently enabled.
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean] true if enabled
|
|
59
|
+
#
|
|
60
|
+
def enabled?
|
|
61
|
+
options.fetch(:enabled, true)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# Convert the extension configuration to a hash for JSON serialization.
|
|
66
|
+
#
|
|
67
|
+
# This hash is passed to the JavaScript TipTap extension.
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash] configuration hash
|
|
70
|
+
#
|
|
71
|
+
def to_config
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Full configuration including name and enabled status.
|
|
77
|
+
#
|
|
78
|
+
# @return [Hash] complete extension configuration
|
|
79
|
+
#
|
|
80
|
+
def to_h
|
|
81
|
+
{
|
|
82
|
+
name: name,
|
|
83
|
+
enabled: enabled?,
|
|
84
|
+
config: to_config
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# JSON representation of the extension.
|
|
90
|
+
#
|
|
91
|
+
# @return [String] JSON string
|
|
92
|
+
#
|
|
93
|
+
def to_json(*args)
|
|
94
|
+
to_h.to_json(*args)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# Default options for this extension.
|
|
101
|
+
#
|
|
102
|
+
# @return [Hash] default options
|
|
103
|
+
#
|
|
104
|
+
def default_options
|
|
105
|
+
{ enabled: true }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inkpen
|
|
4
|
+
module Extensions
|
|
5
|
+
##
|
|
6
|
+
# Code Block with Syntax Highlighting extension.
|
|
7
|
+
#
|
|
8
|
+
# Enhances the standard code block with syntax highlighting using
|
|
9
|
+
# lowlight (highlight.js). Supports multiple programming languages
|
|
10
|
+
# with automatic language detection.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# extension = Inkpen::Extensions::CodeBlockSyntax.new
|
|
14
|
+
#
|
|
15
|
+
# @example With custom languages
|
|
16
|
+
# extension = Inkpen::Extensions::CodeBlockSyntax.new(
|
|
17
|
+
# languages: [:ruby, :javascript, :python, :sql],
|
|
18
|
+
# default_language: :ruby
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# @see https://tiptap.dev/docs/examples/advanced/syntax-highlighting
|
|
22
|
+
# @author Inkpen Team
|
|
23
|
+
# @since 0.2.0
|
|
24
|
+
#
|
|
25
|
+
class CodeBlockSyntax < Base
|
|
26
|
+
##
|
|
27
|
+
# All available programming languages for syntax highlighting.
|
|
28
|
+
# These are loaded from highlight.js via lowlight.
|
|
29
|
+
AVAILABLE_LANGUAGES = %i[
|
|
30
|
+
javascript
|
|
31
|
+
typescript
|
|
32
|
+
ruby
|
|
33
|
+
python
|
|
34
|
+
css
|
|
35
|
+
xml
|
|
36
|
+
html
|
|
37
|
+
json
|
|
38
|
+
bash
|
|
39
|
+
shell
|
|
40
|
+
sql
|
|
41
|
+
markdown
|
|
42
|
+
yaml
|
|
43
|
+
go
|
|
44
|
+
rust
|
|
45
|
+
java
|
|
46
|
+
kotlin
|
|
47
|
+
swift
|
|
48
|
+
php
|
|
49
|
+
c
|
|
50
|
+
cpp
|
|
51
|
+
csharp
|
|
52
|
+
elixir
|
|
53
|
+
erlang
|
|
54
|
+
haskell
|
|
55
|
+
scala
|
|
56
|
+
r
|
|
57
|
+
matlab
|
|
58
|
+
dockerfile
|
|
59
|
+
nginx
|
|
60
|
+
apache
|
|
61
|
+
graphql
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# Default languages to load (for performance).
|
|
66
|
+
DEFAULT_LANGUAGES = %i[
|
|
67
|
+
javascript
|
|
68
|
+
typescript
|
|
69
|
+
ruby
|
|
70
|
+
python
|
|
71
|
+
css
|
|
72
|
+
xml
|
|
73
|
+
json
|
|
74
|
+
bash
|
|
75
|
+
sql
|
|
76
|
+
markdown
|
|
77
|
+
].freeze
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# The unique name of this extension.
|
|
81
|
+
#
|
|
82
|
+
# @return [Symbol] :code_block_syntax
|
|
83
|
+
#
|
|
84
|
+
def name
|
|
85
|
+
:code_block_syntax
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# Languages to enable for syntax highlighting.
|
|
90
|
+
#
|
|
91
|
+
# Only these languages will be loaded from highlight.js to
|
|
92
|
+
# reduce bundle size.
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<Symbol>] enabled language identifiers
|
|
95
|
+
#
|
|
96
|
+
def languages
|
|
97
|
+
options.fetch(:languages, DEFAULT_LANGUAGES)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
##
|
|
101
|
+
# Default language when none is specified.
|
|
102
|
+
#
|
|
103
|
+
# @return [Symbol, nil] default language or nil for auto-detect
|
|
104
|
+
#
|
|
105
|
+
def default_language
|
|
106
|
+
options[:default_language]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Whether to show line numbers in code blocks.
|
|
111
|
+
#
|
|
112
|
+
# @return [Boolean] true to show line numbers
|
|
113
|
+
#
|
|
114
|
+
def line_numbers?
|
|
115
|
+
options.fetch(:line_numbers, false)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
##
|
|
119
|
+
# Whether to show a language selector dropdown.
|
|
120
|
+
#
|
|
121
|
+
# @return [Boolean] true to show language selector
|
|
122
|
+
#
|
|
123
|
+
def show_language_selector?
|
|
124
|
+
options.fetch(:language_selector, true)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Whether to enable copy-to-clipboard button.
|
|
129
|
+
#
|
|
130
|
+
# @return [Boolean] true to show copy button
|
|
131
|
+
#
|
|
132
|
+
def copy_button?
|
|
133
|
+
options.fetch(:copy_button, true)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
##
|
|
137
|
+
# CSS theme for syntax highlighting.
|
|
138
|
+
#
|
|
139
|
+
# @return [String] theme name (e.g., "github", "monokai", "dracula")
|
|
140
|
+
#
|
|
141
|
+
def theme
|
|
142
|
+
options.fetch(:theme, "github")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
##
|
|
146
|
+
# Convert to configuration hash for JavaScript.
|
|
147
|
+
#
|
|
148
|
+
# @return [Hash] configuration for the TipTap extension
|
|
149
|
+
#
|
|
150
|
+
def to_config
|
|
151
|
+
{
|
|
152
|
+
languages: languages,
|
|
153
|
+
defaultLanguage: default_language,
|
|
154
|
+
lineNumbers: line_numbers?,
|
|
155
|
+
languageSelector: show_language_selector?,
|
|
156
|
+
copyButton: copy_button?,
|
|
157
|
+
theme: theme,
|
|
158
|
+
lowlight: {
|
|
159
|
+
languages: languages
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def default_options
|
|
167
|
+
super.merge(
|
|
168
|
+
languages: DEFAULT_LANGUAGES,
|
|
169
|
+
line_numbers: false,
|
|
170
|
+
language_selector: true,
|
|
171
|
+
copy_button: true,
|
|
172
|
+
theme: "github"
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inkpen
|
|
4
|
+
module Extensions
|
|
5
|
+
##
|
|
6
|
+
# DocumentSection extension for TipTap.
|
|
7
|
+
#
|
|
8
|
+
# Provides true content grouping with a semantic H2 title and collapsible
|
|
9
|
+
# content. Unlike the layout Section extension (which controls width/spacing),
|
|
10
|
+
# DocumentSection groups related blocks under a heading for document structure.
|
|
11
|
+
#
|
|
12
|
+
# Features:
|
|
13
|
+
# - Semantic H2 title (integrates with Table of Contents)
|
|
14
|
+
# - Collapsible content with left-gutter toggle
|
|
15
|
+
# - Nesting support (sections within sections, up to 3 levels)
|
|
16
|
+
# - Draggable as a group
|
|
17
|
+
# - Auto-generated IDs for deep linking
|
|
18
|
+
#
|
|
19
|
+
# @example Basic usage
|
|
20
|
+
# extension = Inkpen::Extensions::DocumentSection.new
|
|
21
|
+
#
|
|
22
|
+
# @example With custom options
|
|
23
|
+
# extension = Inkpen::Extensions::DocumentSection.new(
|
|
24
|
+
# default_collapsed: false,
|
|
25
|
+
# allow_nesting: true,
|
|
26
|
+
# max_depth: 3,
|
|
27
|
+
# show_controls: true
|
|
28
|
+
# )
|
|
29
|
+
#
|
|
30
|
+
# @see https://tiptap.dev/docs/editor/api/nodes
|
|
31
|
+
# @author Inkpen Team
|
|
32
|
+
# @since 0.8.0
|
|
33
|
+
#
|
|
34
|
+
class DocumentSection < Base
|
|
35
|
+
##
|
|
36
|
+
# Default maximum nesting depth for sections.
|
|
37
|
+
DEFAULT_MAX_DEPTH = 3
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# The unique name of this extension.
|
|
41
|
+
#
|
|
42
|
+
# @return [Symbol] :document_section
|
|
43
|
+
#
|
|
44
|
+
def name
|
|
45
|
+
:document_section
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# Default collapsed state for new sections.
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean] true if sections start collapsed
|
|
52
|
+
#
|
|
53
|
+
def default_collapsed
|
|
54
|
+
options.fetch(:default_collapsed, false)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Whether sections can be nested within each other.
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean] true to allow nesting
|
|
61
|
+
#
|
|
62
|
+
def allow_nesting?
|
|
63
|
+
options.fetch(:allow_nesting, true)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# Maximum nesting depth for sections.
|
|
68
|
+
#
|
|
69
|
+
# @return [Integer] maximum depth allowed (1-3)
|
|
70
|
+
#
|
|
71
|
+
def max_depth
|
|
72
|
+
depth = options.fetch(:max_depth, DEFAULT_MAX_DEPTH)
|
|
73
|
+
[[depth, 1].max, 3].min # Clamp between 1 and 3
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# Whether to show the collapse toggle control.
|
|
78
|
+
#
|
|
79
|
+
# @return [Boolean] true to show controls
|
|
80
|
+
#
|
|
81
|
+
def show_controls?
|
|
82
|
+
options.fetch(:show_controls, true)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Convert to configuration hash for JavaScript.
|
|
87
|
+
#
|
|
88
|
+
# @return [Hash] configuration for the TipTap extension
|
|
89
|
+
#
|
|
90
|
+
def to_config
|
|
91
|
+
{
|
|
92
|
+
defaultCollapsed: default_collapsed,
|
|
93
|
+
allowNesting: allow_nesting?,
|
|
94
|
+
maxDepth: max_depth,
|
|
95
|
+
showControls: show_controls?
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def default_options
|
|
102
|
+
super.merge(
|
|
103
|
+
default_collapsed: false,
|
|
104
|
+
allow_nesting: true,
|
|
105
|
+
max_depth: DEFAULT_MAX_DEPTH,
|
|
106
|
+
show_controls: true
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Inkpen
|
|
4
|
+
module Extensions
|
|
5
|
+
##
|
|
6
|
+
# Forced Document Structure extension.
|
|
7
|
+
#
|
|
8
|
+
# Enforces a specific document structure where the first element must be
|
|
9
|
+
# a title heading (H1), optionally followed by a subtitle heading (H2).
|
|
10
|
+
# This prevents users from deleting required headings and ensures
|
|
11
|
+
# consistent document structure similar to Medium, Notion, or Dropbox Paper.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage (title only)
|
|
14
|
+
# extension = Inkpen::Extensions::ForcedDocument.new
|
|
15
|
+
#
|
|
16
|
+
# @example With subtitle enabled
|
|
17
|
+
# extension = Inkpen::Extensions::ForcedDocument.new(
|
|
18
|
+
# subtitle: true,
|
|
19
|
+
# title_placeholder: "Your title here...",
|
|
20
|
+
# subtitle_placeholder: "Add a subtitle..."
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# @example Custom heading levels
|
|
24
|
+
# extension = Inkpen::Extensions::ForcedDocument.new(
|
|
25
|
+
# title_level: 1,
|
|
26
|
+
# subtitle_level: 2,
|
|
27
|
+
# subtitle: true
|
|
28
|
+
# )
|
|
29
|
+
#
|
|
30
|
+
# @see https://tiptap.dev/docs/examples/advanced/forced-content-structure
|
|
31
|
+
# @author Inkpen Team
|
|
32
|
+
# @since 0.2.0
|
|
33
|
+
#
|
|
34
|
+
class ForcedDocument < Base
|
|
35
|
+
##
|
|
36
|
+
# Default heading level for the title (H1).
|
|
37
|
+
DEFAULT_TITLE_LEVEL = 1
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# Default heading level for the subtitle (H2).
|
|
41
|
+
DEFAULT_SUBTITLE_LEVEL = 2
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Default placeholder text for the title heading.
|
|
45
|
+
DEFAULT_TITLE_PLACEHOLDER = "Untitled"
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# Default placeholder text for the subtitle heading.
|
|
49
|
+
DEFAULT_SUBTITLE_PLACEHOLDER = "Add a subtitle..."
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# The unique name of this extension.
|
|
53
|
+
#
|
|
54
|
+
# @return [Symbol] :forced_document
|
|
55
|
+
#
|
|
56
|
+
def name
|
|
57
|
+
:forced_document
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# The heading level for the title (first heading).
|
|
62
|
+
#
|
|
63
|
+
# @return [Integer] heading level (1-6), default 1
|
|
64
|
+
#
|
|
65
|
+
def title_level
|
|
66
|
+
options.fetch(:title_level, options.fetch(:heading_level, DEFAULT_TITLE_LEVEL))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# The heading level for the subtitle (second heading).
|
|
71
|
+
#
|
|
72
|
+
# @return [Integer] heading level (1-6), default 2
|
|
73
|
+
#
|
|
74
|
+
def subtitle_level
|
|
75
|
+
options.fetch(:subtitle_level, DEFAULT_SUBTITLE_LEVEL)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
# Whether subtitle is enabled.
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean] true if subtitle heading should be enforced
|
|
82
|
+
#
|
|
83
|
+
def subtitle?
|
|
84
|
+
options.fetch(:subtitle, false)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
##
|
|
88
|
+
# Placeholder text shown in the title heading when empty.
|
|
89
|
+
#
|
|
90
|
+
# @return [String] placeholder text
|
|
91
|
+
#
|
|
92
|
+
def title_placeholder
|
|
93
|
+
options.fetch(:title_placeholder, options.fetch(:placeholder, DEFAULT_TITLE_PLACEHOLDER))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
##
|
|
97
|
+
# Placeholder text shown in the subtitle heading when empty.
|
|
98
|
+
#
|
|
99
|
+
# @return [String] placeholder text
|
|
100
|
+
#
|
|
101
|
+
def subtitle_placeholder
|
|
102
|
+
options.fetch(:subtitle_placeholder, DEFAULT_SUBTITLE_PLACEHOLDER)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# Whether to allow the title heading to be deleted.
|
|
107
|
+
#
|
|
108
|
+
# @return [Boolean] false by default (title cannot be deleted)
|
|
109
|
+
#
|
|
110
|
+
def allow_title_deletion?
|
|
111
|
+
options.fetch(:allow_deletion, false)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
##
|
|
115
|
+
# Whether to allow the subtitle heading to be deleted.
|
|
116
|
+
#
|
|
117
|
+
# @return [Boolean] true by default (subtitle can be deleted/cleared)
|
|
118
|
+
#
|
|
119
|
+
def allow_subtitle_deletion?
|
|
120
|
+
options.fetch(:allow_subtitle_deletion, true)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
##
|
|
124
|
+
# Document structure schema.
|
|
125
|
+
#
|
|
126
|
+
# Defines the required structure:
|
|
127
|
+
# - Without subtitle: heading block*
|
|
128
|
+
# - With subtitle: heading heading? block*
|
|
129
|
+
#
|
|
130
|
+
# @return [String] ProseMirror content expression
|
|
131
|
+
#
|
|
132
|
+
def content_expression
|
|
133
|
+
if subtitle?
|
|
134
|
+
"heading heading? block*"
|
|
135
|
+
else
|
|
136
|
+
"heading block*"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
##
|
|
141
|
+
# Convert to configuration hash for JavaScript.
|
|
142
|
+
#
|
|
143
|
+
# @return [Hash] configuration for the TipTap extension
|
|
144
|
+
#
|
|
145
|
+
def to_config
|
|
146
|
+
config = {
|
|
147
|
+
titleLevel: title_level,
|
|
148
|
+
titlePlaceholder: title_placeholder,
|
|
149
|
+
allowDeletion: allow_title_deletion?,
|
|
150
|
+
contentExpression: content_expression,
|
|
151
|
+
subtitle: subtitle?
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if subtitle?
|
|
155
|
+
config.merge!(
|
|
156
|
+
subtitleLevel: subtitle_level,
|
|
157
|
+
subtitlePlaceholder: subtitle_placeholder,
|
|
158
|
+
allowSubtitleDeletion: allow_subtitle_deletion?
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Legacy support for headingLevel
|
|
163
|
+
config[:headingLevel] = title_level
|
|
164
|
+
|
|
165
|
+
config
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def default_options
|
|
171
|
+
super.merge(
|
|
172
|
+
title_level: DEFAULT_TITLE_LEVEL,
|
|
173
|
+
subtitle_level: DEFAULT_SUBTITLE_LEVEL,
|
|
174
|
+
title_placeholder: DEFAULT_TITLE_PLACEHOLDER,
|
|
175
|
+
subtitle_placeholder: DEFAULT_SUBTITLE_PLACEHOLDER,
|
|
176
|
+
subtitle: false,
|
|
177
|
+
allow_deletion: false,
|
|
178
|
+
allow_subtitle_deletion: true
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|