ordars_rails_editor 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.
@@ -0,0 +1,84 @@
1
+ <%# ordars-rails-editor 본체 파셜 — 헬퍼 ore_editor 가 렌더.
2
+ locals: input_name, content, placeholder, upload_url, attach_name,
3
+ max_images, max_videos, max_mb, show_attach, editor_id %>
4
+ <div class="ore" id="<%= editor_id %>"
5
+ data-ore-editor
6
+ data-ore-placeholder="<%= placeholder %>"
7
+ <% if upload_url.present? %>data-ore-upload-url="<%= upload_url %>"<% end %>
8
+ <% if attach_name.present? %>data-ore-attach-name="<%= attach_name %>"<% end %>
9
+ data-ore-max-images="<%= max_images %>"
10
+ data-ore-max-videos="<%= max_videos %>"
11
+ data-ore-max-mb="<%= max_mb %>">
12
+
13
+ <%# 저장 필드 — 에디터가 update/submit 마다 HTML 을 동기화 %>
14
+ <textarea class="ore-input" name="<%= input_name %>" hidden aria-hidden="true"><%= content %></textarea>
15
+
16
+ <div class="ore-toolbar">
17
+ <select class="ore-select ore-fontsize" data-tip="글자 크기">
18
+ <option value="">크기</option>
19
+ <option value="13px">13px</option>
20
+ <option value="15px">15px</option>
21
+ <option value="16px">16px</option>
22
+ <option value="18px">18px</option>
23
+ <option value="22px">22px</option>
24
+ <option value="28px">28px</option>
25
+ </select>
26
+ <span class="ore-sep"></span>
27
+ <button type="button" class="ore-btn" data-cmd="bulletList" data-tip="글머리 목록"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="4" cy="6" r="1.4" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1.4" fill="currentColor" stroke="none"/><path d="M9 6h12M9 12h12M9 18h12"/></svg></button>
28
+ <button type="button" class="ore-btn" data-cmd="orderedList" data-tip="번호 목록"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10 6h11M10 12h11M10 18h11"/><text x="2" y="8" font-size="7.5" fill="currentColor" stroke="none" font-weight="700">1</text><text x="2" y="14.5" font-size="7.5" fill="currentColor" stroke="none" font-weight="700">2</text><text x="2" y="21" font-size="7.5" fill="currentColor" stroke="none" font-weight="700">3</text></svg></button>
29
+ <button type="button" class="ore-btn" data-cmd="blockquote" data-tip="인용">❝</button>
30
+ <span class="ore-sep"></span>
31
+ <button type="button" class="ore-btn" data-cmd="bold" data-tip="굵게 (Ctrl+B)"><b>B</b></button>
32
+ <button type="button" class="ore-btn" data-cmd="italic" data-tip="기울임 (Ctrl+I)"><i>I</i></button>
33
+ <button type="button" class="ore-btn" data-cmd="strike" data-tip="취소선"><s>S</s></button>
34
+ <button type="button" class="ore-btn" data-cmd="underline" data-tip="밑줄 (Ctrl+U)"><u>U</u></button>
35
+ <button type="button" class="ore-btn" data-cmd="code" data-tip="코드">&lt;/&gt;</button>
36
+ <span class="ore-sep"></span>
37
+ <span class="ore-btnwrap">
38
+ <button type="button" class="ore-btn" data-pop=".ore-colorpop" data-tip="글자색"><span style="border-bottom:3px solid #dc2626;">A</span></button>
39
+ <span class="ore-pop ore-colorpop"><span class="ore-pal ore-colorpal"></span></span>
40
+ </span>
41
+ <span class="ore-btnwrap">
42
+ <button type="button" class="ore-btn" data-pop=".ore-hlpop" data-tip="형광펜"><span style="background:#fef08a;border-radius:3px;padding:0 3px;">가</span></button>
43
+ <span class="ore-pop ore-hlpop"><span class="ore-pal ore-hlpal"></span></span>
44
+ </span>
45
+ <button type="button" class="ore-btn" data-cmd="link" data-tip="링크 삽입/해제"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg></button>
46
+ <button type="button" class="ore-btn" data-cmd="hr" data-tip="구분선">—</button>
47
+ <span class="ore-sep"></span>
48
+ <button type="button" class="ore-btn" data-cmd="alignLeft" data-tip="왼쪽 정렬"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M3 10h12M3 14h18M3 18h12"/></svg></button>
49
+ <button type="button" class="ore-btn" data-cmd="alignCenter" data-tip="가운데 정렬"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M6 10h12M3 14h18M6 18h12"/></svg></button>
50
+ <button type="button" class="ore-btn" data-cmd="alignRight" data-tip="오른쪽 정렬"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M9 10h12M3 14h18M9 18h12"/></svg></button>
51
+ <button type="button" class="ore-btn" data-cmd="alignJustify" data-tip="양쪽 정렬"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M3 10h18M3 14h18M3 18h18"/></svg></button>
52
+ <span class="ore-sep"></span>
53
+ <button type="button" class="ore-btn ore-imgbtn" data-tip="이미지 본문 삽입"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg></button>
54
+ <span class="ore-tail">
55
+ <button type="button" class="ore-btn" data-cmd="undo" data-tip="실행 취소 (Ctrl+Z)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14L4 9l5-5"/><path d="M4 9h10a6 6 0 010 12h-3"/></svg></button>
56
+ <button type="button" class="ore-btn" data-cmd="redo" data-tip="다시 실행 (Ctrl+Y)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14l5-5-5-5"/><path d="M20 9H10a6 6 0 000 12h3"/></svg></button>
57
+ <span class="ore-sep"></span>
58
+ <button type="button" class="ore-btn ore-btn--wide ore-htmlbtn" data-cmd="html" data-tip="HTML 소스 편집">HTML</button>
59
+ </span>
60
+ </div>
61
+
62
+ <div class="ore-content"></div>
63
+ <textarea class="ore-html" spellcheck="false"></textarea>
64
+
65
+ <% if show_attach %>
66
+ <div class="ore-attachbar">
67
+ <span class="grp"><button type="button" class="ore-filebtn" data-tip="파일 첨부 (드래그도 가능)">
68
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>
69
+ 파일</button></span>
70
+ <span class="grp">
71
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
72
+ 이미지 <span class="ore-cnt-img">0</span>/<%= max_images %> <span class="ore-gauge ore-g-img"><i></i></span></span>
73
+ <span class="grp">
74
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="3"/><path d="M10 9l5 3-5 3z" fill="currentColor" stroke="none"/></svg>
75
+ 동영상 <span class="ore-cnt-vid">0</span>/<%= max_videos %> <span class="ore-gauge ore-g-vid"><i></i></span></span>
76
+ <span class="cap">
77
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
78
+ 첨부파일용량 <b class="ore-cap-used">0.00MB</b>/<%= max_mb %>MB <span class="ore-gauge ore-g-cap" style="width:110px;"><i></i></span></span>
79
+ </div>
80
+ <div class="ore-attachlist">
81
+ <div class="ore-attach-empty">첨부된 파일이 없습니다. — [파일] 버튼 또는 이 영역에 드래그</div>
82
+ </div>
83
+ <% end %>
84
+ </div>
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ordars-rails-editor importmap pins — gem 로드 시 자동 등록
4
+ pin "ordars_rails_editor", to: "ordars_rails_editor/index.js"
5
+ pin "ordars_rails_editor/tiptap", to: "ordars_rails_editor/tiptap.js", preload: false
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OrdarsRailsEditor
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace OrdarsRailsEditor
6
+
7
+ initializer "ordars_rails_editor.assets" do |app|
8
+ if app.config.respond_to?(:assets)
9
+ app.config.assets.paths << root.join("app/assets/stylesheets")
10
+ app.config.assets.precompile += %w[ordars_rails_editor.css] if app.config.assets.respond_to?(:precompile)
11
+ end
12
+
13
+ # importmap 앱: JS 를 애셋 경로로 노출
14
+ if app.config.respond_to?(:importmap)
15
+ app.config.assets.paths << root.join("app/javascript")
16
+ end
17
+ end
18
+
19
+ initializer "ordars_rails_editor.importmap", before: "importmap" do |app|
20
+ if app.config.respond_to?(:importmap)
21
+ app.config.importmap.paths << root.join("config/importmap.rb")
22
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
23
+ end
24
+ end
25
+
26
+ initializer "ordars_rails_editor.helpers" do
27
+ ActiveSupport.on_load(:action_view) do
28
+ include OrdarsRailsEditor::EditorHelper if defined?(OrdarsRailsEditor::EditorHelper)
29
+ end
30
+ end
31
+
32
+ initializer "ordars_rails_editor.view_paths" do
33
+ ActiveSupport.on_load(:action_controller) do
34
+ append_view_path OrdarsRailsEditor::Engine.root.join("app/views")
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OrdarsRailsEditor
4
+ # 에디터 저장 HTML 서버측 새니타이저 — 저장형 XSS 방어의 정본.
5
+ #
6
+ # ⭐호스트 앱은 «저장 직전» 반드시 이걸 통과시킨다:
7
+ # post.content = OrdarsRailsEditor::Sanitizer.clean(params[:content])
8
+ # 클라이언트(에디터)가 만드는 HTML 스키마와 이 화이트리스트는 한 쌍 — 에디터에 서식을
9
+ # 추가하면 여기 허용 목록도 함께 넓힌다(안 넓히면 저장 시 조용히 벗겨진다 — 의도된 동작).
10
+ #
11
+ # style 속성은 통째 허용하지 않고 «속성 단위»로 거른다(color/background-color/font-size/text-align).
12
+ module Sanitizer
13
+ ALLOWED_TAGS = %w[p br strong b em i s del u code pre blockquote ul ol li h1 h2 h3 hr a img span].freeze
14
+ ALLOWED_ATTRIBUTES = %w[href target rel src alt width height style data-file-id].freeze
15
+ ALLOWED_STYLE_PROPS = %w[color background-color font-size text-align].freeze
16
+ ALLOWED_URI_SCHEMES = %w[http https mailto tel].freeze
17
+
18
+ module_function
19
+
20
+ def clean(html)
21
+ return "" if html.nil? || html.to_s.strip.empty?
22
+
23
+ sanitized = base_sanitizer.sanitize(
24
+ html.to_s,
25
+ tags: ALLOWED_TAGS,
26
+ attributes: ALLOWED_ATTRIBUTES,
27
+ scrubber: nil
28
+ )
29
+
30
+ frag = Loofah.fragment(sanitized)
31
+
32
+ # style 은 허용 속성만 통과
33
+ frag.css("[style]").each do |node|
34
+ filtered = filter_style(node["style"])
35
+ filtered.empty? ? node.remove_attribute("style") : (node["style"] = filtered)
36
+ end
37
+
38
+ # 링크: 스킴 화이트리스트 + 새 탭 안전 rel
39
+ frag.css("a").each do |a|
40
+ href = a["href"].to_s.strip
41
+ scheme = href[/\A([a-z][a-z0-9+.-]*):/i, 1]&.downcase
42
+ if href.empty? || (scheme && !ALLOWED_URI_SCHEMES.include?(scheme))
43
+ a.remove_attribute("href")
44
+ end
45
+ a["rel"] = "noopener noreferrer" if a["target"].to_s == "_blank"
46
+ end
47
+
48
+ frag.to_s
49
+ end
50
+
51
+ def filter_style(style)
52
+ style.to_s.split(";").filter_map do |decl|
53
+ prop, value = decl.split(":", 2)
54
+ next if prop.nil? || value.nil?
55
+ prop = prop.strip.downcase
56
+ value = value.strip
57
+ next unless ALLOWED_STYLE_PROPS.include?(prop)
58
+ next if value.match?(/url\s*\(|expression|javascript/i)
59
+ "#{prop}: #{value}"
60
+ end.join("; ")
61
+ end
62
+
63
+ def base_sanitizer
64
+ @base_sanitizer ||= Rails::HTML5::SafeListSanitizer.new
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OrdarsRailsEditor
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ordars_rails_editor/version"
4
+ require_relative "ordars_rails_editor/sanitizer"
5
+ require_relative "ordars_rails_editor/engine"
6
+
7
+ module OrdarsRailsEditor
8
+ class Error < StandardError; end
9
+
10
+ # 기본 첨부 한도 (헬퍼 옵션으로 개별 지정 가능)
11
+ mattr_accessor :default_max_images, default: 100
12
+ mattr_accessor :default_max_videos, default: 50
13
+ mattr_accessor :default_max_total_mb, default: 50
14
+
15
+ def self.configure
16
+ yield self
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ordars_rails_editor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - reshacs
8
+ bindir: bin
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
+ description: A board-style WYSIWYG editor for Rails apps. Ships a bundled Tiptap/ProseMirror
27
+ core (no external CDN), an etoland-style toolbar with color/highlight palettes and
28
+ tooltips, an attachment panel with upload adapter, HTML source mode, a server-side
29
+ sanitizer, and CSS-variable theming.
30
+ email:
31
+ - allevery80@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - MIT-LICENSE
37
+ - README.md
38
+ - app/assets/stylesheets/ordars_rails_editor.css
39
+ - app/helpers/ordars_rails_editor/editor_helper.rb
40
+ - app/javascript/ordars_rails_editor/index.js
41
+ - app/javascript/ordars_rails_editor/tiptap.js
42
+ - app/views/ordars_rails_editor/_editor.html.erb
43
+ - config/importmap.rb
44
+ - lib/ordars_rails_editor.rb
45
+ - lib/ordars_rails_editor/engine.rb
46
+ - lib/ordars_rails_editor/sanitizer.rb
47
+ - lib/ordars_rails_editor/version.rb
48
+ homepage: https://github.com/ordars/ordars-rails-editor
49
+ licenses:
50
+ - MIT
51
+ metadata: {}
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 3.0.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.6.9
67
+ specification_version: 4
68
+ summary: Rich text editor for Rails (Tiptap core + self-contained toolbar/attachment
69
+ shell)
70
+ test_files: []