card-mod-content 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bb52b2764cccee68e9de8d73edf69ae8be71529d81728b009fbf2f93bac63964
4
+ data.tar.gz: 576999f00236a239917c9671a301358987554468e9e6b1640616f3c5a6cd1134
5
+ SHA512:
6
+ metadata.gz: d375445000c6b0de05d4f455f24b45dde8999bc38e1e5021342865e74aa8d8e02ad057460411fa79c52892d15bdc97c0897ed4fe59a8a5cb37ab5ab08b234575
7
+ data.tar.gz: b01ba00743f9392eee0ace1b6717fcbd5a2ca21c5b4b619a01ef51fd66ecb4e3c15628d6d90e90d0850cff22a8693ef4a2e3c516ae2c6b2c8f16c539cd82d813
@@ -0,0 +1,27 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ class Card
4
+ class Content
5
+ module Chunk
6
+ # These are basic chunks that have a pattern and can be protected.
7
+ # They are used by rendering process to prevent wiki rendering
8
+ # occuring within literal areas such as <code> and <pre> blocks
9
+ # and within HTML tags.
10
+ class EscapedLiteral < Abstract
11
+ FULL_RE = { "[" => /\A\\\[\[[^\]]*\]\]/,
12
+ "{" => /\A\\\{\{[^\}]*\}\}/ }.freeze
13
+ Card::Content::Chunk.register_class self,
14
+ prefix_re: '\\\\(?:\\[\\[|\\{\\{)',
15
+ idx_char: '\\'
16
+
17
+ def self.full_re prefix
18
+ FULL_RE[prefix[1, 1]]
19
+ end
20
+
21
+ def interpret match, _content
22
+ @process_chunk = match[0].sub(/^\\(.)/, format.escape_literal('\1'))
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ class Card
4
+ class Content
5
+ module Chunk
6
+ # These are basic chunks that have a pattern and can be protected.
7
+ # This chunk is used for markdown processing to ensure that
8
+ # the escaping survives the markdown rendering.
9
+ class KeepEscapedLiteral < Abstract
10
+ FULL_RE = { "[" => /\A\\\[\[[^\]]*\]\]/,
11
+ "{" => /\A\\\{\{[^\}]*\}\}/ }.freeze
12
+ Card::Content::Chunk.register_class self,
13
+ prefix_re: '\\\\(?:\\[\\[|\\{\\{)',
14
+ idx_char: '\\'
15
+
16
+ def self.full_re prefix
17
+ FULL_RE[prefix[1, 1]]
18
+ end
19
+
20
+ def interpret match, _content
21
+ @process_chunk = match[0].sub(/^\\(.)/, '\\\\\\\\\1')
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,124 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ # require File.expand_path("../reference", __FILE__)
4
+ load File.expand_path("../reference.rb", __FILE__)
5
+
6
+ class Card
7
+ class Content
8
+ module Chunk
9
+ # extend ActiveSupport::Autoload
10
+ # autoload :Reference , "reference"
11
+
12
+ class Link < Card::Content::Chunk::Reference
13
+ CODE = "L".freeze # L for "Link"
14
+ attr_reader :link_text
15
+ # Groups: $1, [$2]: [[$1]] or [[$1|$2]] or $3, $4: [$3][$4]
16
+ Card::Content::Chunk.register_class self,
17
+ prefix_re: '\\[\\[',
18
+ full_re: /\A\[\[([^\]]+)\]\]/,
19
+ idx_char: "["
20
+ def reference_code
21
+ CODE
22
+ end
23
+
24
+ def interpret match, _content
25
+ target, @link_text = target_and_link_text match[1]
26
+
27
+ @link_text = objectify @link_text
28
+ if target.match? %r{^(/|https?:|mailto:)}
29
+ @explicit_link = objectify target
30
+ else
31
+ @name = target
32
+ end
33
+ end
34
+
35
+ def target_and_link_text raw_syntax
36
+ return unless raw_syntax
37
+
38
+ if (i = divider_index raw_syntax) # [[A | B]]
39
+ [raw_syntax[0..(i - 1)], raw_syntax[(i + 1)..-1]] # [A, B]
40
+ else # [[ A ]]
41
+ [raw_syntax, nil] # [A, nil]
42
+ end
43
+ end
44
+
45
+ def divider_index string
46
+ # there's probably a better way to do the following.
47
+ # point is to find the first pipe that's not inside an nest
48
+ return unless string.index "|"
49
+ string_copy = string.dup
50
+ string.scan(/\{\{[^\}]*\}\}/) do |incl|
51
+ string_copy.gsub! incl, ("x" * incl.length)
52
+ end
53
+ string_copy.index "|"
54
+ end
55
+
56
+ # view options
57
+ def options
58
+ link_text ? { title: link_text } : {}
59
+ end
60
+
61
+ def objectify raw
62
+ return unless raw
63
+ raw.strip!
64
+ if raw.match?(/(^|[^\\])\{\{/)
65
+ Card::Content.new raw, format
66
+ else
67
+ raw
68
+ end
69
+ end
70
+
71
+ def render_link view: :link, explicit_link_opts: {}
72
+ @link_text = render_obj @link_text
73
+
74
+ if @explicit_link
75
+ @explicit_link = render_obj @explicit_link
76
+ format.link_to_resource @explicit_link, @link_text, explicit_link_opts
77
+ elsif @name
78
+ format.with_nest_mode :normal do
79
+ format.nest referee_name, options.merge(view: view)
80
+ end
81
+ end
82
+ end
83
+
84
+ def link_target
85
+ if @explicit_link
86
+ render_obj @explicit_link
87
+ elsif @name
88
+ referee_name
89
+ end
90
+ end
91
+
92
+ def process_chunk
93
+ @process_chunk ||= render_link
94
+ end
95
+
96
+ def inspect
97
+ "<##{self.class}:e[#{@explicit_link}]n[#{@name}]l[#{@link_text}]" \
98
+ "p[#{@process_chunk}] txt:#{@text}>"
99
+ end
100
+
101
+ def replace_reference old_name, new_name
102
+ replace_name_reference old_name, new_name
103
+ replace_link_text old_name, new_name
104
+ @text =
105
+ @link_text.nil? ? "[[#{referee_name}]]" : "[[#{referee_name}|#{@link_text}]]"
106
+ end
107
+
108
+ def replace_link_text old_name, new_name
109
+ if @link_text.is_a?(Card::Content)
110
+ @link_text.find_chunks(Card::Content::Chunk::Reference).each do |chunk|
111
+ chunk.replace_reference old_name, new_name
112
+ end
113
+ elsif @link_text.present?
114
+ @link_text = old_name.to_name.sub_in(@link_text, with: new_name)
115
+ end
116
+ end
117
+
118
+ def explicit_link?
119
+ @explicit_link
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,126 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ # require File.expand_path("../reference", __FILE__)
4
+
5
+ class Card
6
+ class Content
7
+ module Chunk
8
+ # Handler for nest chunks: {{example}}
9
+ class Nest < Reference
10
+ attr_reader :options
11
+ DEFAULT_OPTION = :view # a value without a key is interpreted as view
12
+
13
+ Chunk.register_class(self, prefix_re: '\\{\\{',
14
+ full_re: /\A\{\{([^\{\}]*)\}\}/,
15
+ idx_char: "{")
16
+
17
+ def interpret match, _content
18
+ in_brackets = strip_tags match[1]
19
+ name, @opt_lists = in_brackets.split "|", 2
20
+ name = name.to_s.strip
21
+ if name.match?(/^\#/)
22
+ @process_chunk = name.match?(/^\#\#/) ? "" : visible_comment(in_brackets)
23
+ else
24
+ @options = interpret_options.merge nest_name: name, nest_syntax: in_brackets
25
+ @name = name
26
+ end
27
+ end
28
+
29
+ def strip_tags string
30
+ # note: not using ActionView's strip_tags here
31
+ # because this needs to be super fast.
32
+ string.gsub(/\<[^\>]*\>/, "")
33
+ end
34
+
35
+ def visible_comment message
36
+ "<!-- #{CGI.escapeHTML message} -->"
37
+ end
38
+
39
+ def interpret_options
40
+ raw_options = @opt_lists.to_s.split("|").reverse
41
+ raw_options.inject(nil) do |prev_level, level_options|
42
+ interpret_piped_options level_options, prev_level
43
+ end || {}
44
+ end
45
+
46
+ def interpret_piped_options list_string, items
47
+ options_hash = items.nil? ? {} : { items: items }
48
+ option_string_to_hash list_string, options_hash
49
+ options_hash
50
+ end
51
+
52
+ def option_string_to_hash list_string, options_hash
53
+ each_option(list_string) do |key, value|
54
+ key = key.to_sym
55
+ if key == :item
56
+ options_hash[:items] ||= {}
57
+ options_hash[:items][:view] = value
58
+ elsif Card::View::Options.shark_keys.include? key
59
+ options_hash[key] = value
60
+ # else
61
+ # handle other keys
62
+ end
63
+ end
64
+ end
65
+
66
+ def inspect
67
+ "<##{self.class}:n[#{@name}] p[#{@process_chunk}] txt:#{@text}>"
68
+ end
69
+
70
+ def process_chunk
71
+ return @process_chunk if @process_chunk
72
+
73
+ referee_name
74
+ @processed = format.content_nest(@options)
75
+ # this is not necessarily text, sometimes objects for json
76
+ end
77
+
78
+ def replace_reference old_name, new_name
79
+ replace_name_reference old_name, new_name
80
+ nest_body = [@name.to_s, @opt_lists].compact * "|"
81
+ @text = "{{#{nest_body}}}"
82
+ end
83
+
84
+ def explicit_view= view
85
+ return if @options[:view]
86
+ # could check to make sure it's not already the default...
87
+ if @text.match?(/\|/)
88
+ @text.sub! "|", "|#{view};"
89
+ else
90
+ @text.sub! "}}", "|#{view}}}"
91
+ end
92
+ end
93
+
94
+ def main?
95
+ nest_name == "_main"
96
+ end
97
+
98
+ def nest_name
99
+ options&.dig :nest_name
100
+ end
101
+
102
+ def self.gsub string
103
+ string.gsub(/\{\{([^\}]*)\}\}/) do |_match|
104
+ yield(Regexp.last_match[1])
105
+ end
106
+ end
107
+
108
+ def raw_options
109
+ @opt_lists
110
+ end
111
+
112
+ private
113
+
114
+ def each_option attr_string
115
+ return if attr_string.blank?
116
+ attr_string.strip.split(";").each do |pair|
117
+ # key is optional for view option
118
+ value, key = pair.split(":", 2).reverse
119
+ key ||= self.class::DEFAULT_OPTION.to_s
120
+ yield key.strip, value.strip
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,95 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ class Card
4
+ class Content
5
+ module Chunk
6
+ # This should find +Alfred+ in expressions like
7
+ # 1) {"name":"Alfred"}
8
+ # 2a) {"name":["in","Alfred"]}
9
+ # 3a) {"plus_right":["Alfred"]}
10
+ # but not in
11
+ # 2b) "content":"foo", "Alfred":"bar"
12
+ # 3b) {"name":["Alfred", "Toni"]} ("Alfred" is an operator here)
13
+ # It's not possible to distinguish between 2a) and 2b) or 3a) and 3b) with a
14
+ # simple regex, hence we use a too general regex and check for query keywords
15
+ # after the match, which of course means that we don't find references with
16
+ # query keywords as name
17
+
18
+ require File.expand_path("reference", __dir__)
19
+ class QueryReference < Reference
20
+ QUERY_KEYWORDS = ::Set.new(
21
+ (
22
+ ::Card::Query::MODIFIERS.keys +
23
+ ::Card::Query::OPERATORS.keys +
24
+ ::Card::Query::ATTRIBUTES.keys +
25
+ ::Card::Query::CONJUNCTIONS.keys +
26
+ %w[desc asc count]
27
+ ).map(&:to_s)
28
+ )
29
+
30
+ Card::Content::Chunk.register_class(
31
+ self, prefix_re: '(?<=[:,\\[])\\s*"',
32
+ # we check for colon, comma or square bracket before a quote
33
+ # we have to use a lookbehind, otherwise
34
+ # if the colon matches it would be
35
+ # identified mistakenly as an URI chunk
36
+ full_re: /\A\s*"([^"]+)"/,
37
+ idx_char: '"'
38
+ )
39
+
40
+ # OPTIMIZE: instead of comma or square bracket check for operator followed
41
+ # by comma or "plus_right"|"plus_left"|"plus" followed by square bracket
42
+ # something like
43
+ # prefix_patterns = [
44
+ # "\"\\s*(?:#{Card::Query::OPERATORS.keys.join('|')})\"\\s*,",
45
+ # "\"\\s*(?:#{Card::Query::PLUS_ATTRIBUTES}.keys
46
+ # .join('|')})\\s*:\\s*\\[\\s*",
47
+ # "\"\\s*(?:#{(QUERY_KEYWORDS - Card::Query::PLUS_ATTRIBUTES)
48
+ # .join('|')})\"\\s*:",
49
+ # ]
50
+ # prefix_re: '(?<=#{prefix_patterns.join('|')})\\s*"'
51
+ # But: What do we do with the "in" operator? After the first value there is
52
+ # no prefix which we can use to detect the following values as
53
+ # QueryReference chunks
54
+
55
+ class << self
56
+ def full_match content, prefix
57
+ # matches cardnames that are not keywords
58
+ # FIXME: would not match cardnames that are keywords
59
+ match, offset = super(content, prefix)
60
+ return if !match || keyword?(match[1])
61
+
62
+ [match, offset]
63
+ end
64
+
65
+ def keyword? str
66
+ return unless str
67
+
68
+ QUERY_KEYWORDS.include?(str.tr(" ", "_").downcase)
69
+ end
70
+ end
71
+
72
+ def interpret match, _content
73
+ @name = match[1]
74
+ end
75
+
76
+ def process_chunk
77
+ @process_chunk ||= @text
78
+ end
79
+
80
+ def inspect
81
+ "<##{self.class}:n[#{@name}] p[#{@process_chunk}] txt:#{@text}>"
82
+ end
83
+
84
+ def replace_reference old_name, new_name
85
+ replace_name_reference old_name, new_name
86
+ @text = "\"#{@name}\""
87
+ end
88
+
89
+ def reference_code
90
+ "Q" # for "Query"
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,60 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ class Card
4
+ class Content
5
+ module Chunk
6
+ class Reference < Abstract
7
+ attr_writer :referee_name
8
+ attr_accessor :name
9
+
10
+ def referee_name
11
+ return if name.nil?
12
+ @referee_name ||= referee_name_from_rendered(render_obj(name))
13
+ @referee_name = @referee_name.absolute(card.name).to_name
14
+ rescue Card::Error::NotFound
15
+ # do not break on missing id/codename references.
16
+ end
17
+
18
+ def referee_name_from_rendered rendered_name
19
+ ref_card = fetch_referee_card rendered_name
20
+ ref_card ? ref_card.name : rendered_name.to_name
21
+ end
22
+
23
+ def referee_card
24
+ @referee_card ||= referee_name && Card.fetch(referee_name)
25
+ end
26
+
27
+ def replace_name_reference old_name, new_name
28
+ @referee_card = nil
29
+ @referee_name = nil
30
+ if name.is_a? Card::Content
31
+ name.find_chunks(Chunk::Reference).each do |chunk|
32
+ chunk.replace_reference old_name, new_name
33
+ end
34
+ else
35
+ @name = name.to_name.swap old_name, new_name
36
+ end
37
+ end
38
+
39
+ def render_obj raw
40
+ if format && raw.is_a?(Card::Content)
41
+ format.process_content raw
42
+ else
43
+ raw
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def fetch_referee_card rendered_name
50
+ case rendered_name # FIXME: this should be standard fetch option.
51
+ when /^\~(\d+)$/ # get by id
52
+ Card.fetch Regexp.last_match(1).to_i
53
+ when /^\:(\w+)$/ # get by codename
54
+ Card.fetch Regexp.last_match(1).to_sym
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,145 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require "uri"
4
+
5
+ # This wiki chunk matches arbitrary URIs, using patterns from the Ruby URI
6
+ # modules.
7
+ # It parses out a variety of fields that could be used by formats to format
8
+ # the links in various ways (shortening domain names, hiding email addresses)
9
+ # It matches email addresses and host.com.au domains without schemes (http://)
10
+ # but adds these on as required.
11
+ #
12
+ # The heuristic used to match a URI is designed to err on the side of caution.
13
+ # That is, it is more likely to not autolink a URI than it is to accidently
14
+ # autolink something that is not a URI. The reason behind this is it is easier
15
+ # to force a URI link by prefixing 'http://' to it than it is to escape and
16
+ # incorrectly marked up non-URI.
17
+ #
18
+ # I'm using a part of the [ISO 3166-1 Standard][iso3166] for country name
19
+ # suffixes.
20
+ # The generic names are from www.bnoack.com/data/countrycode2.html)
21
+ # [iso3166]: http://geotags.com/iso3166/
22
+ module Card::Content::Chunk
23
+ class Uri < Abstract
24
+ SCHEMES = %w[irc http https ftp ssh git sftp file ldap ldaps mailto].freeze
25
+
26
+ REJECTED_PREFIX_RE = %w{! ": " ' ](}.map { |s| Regexp.escape s } * "|"
27
+
28
+ attr_reader :uri, :link_text
29
+ delegate :to, :scheme, :host, :port, :path, :query, :fragment, to: :uri
30
+
31
+ Card::Content::Chunk.register_class(
32
+ self, prefix_re: "(?:(?!#{REJECTED_PREFIX_RE})(?:#{SCHEMES * '|'})\\:)",
33
+ full_re: /\A#{::URI.regexp(SCHEMES)}/,
34
+ idx_char: ":"
35
+ )
36
+
37
+ class << self
38
+ def full_match content, prefix
39
+ prepend_str = if prefix[-1, 1] != ":" && config[:prepend_str]
40
+ config[:prepend_str]
41
+ else
42
+ ""
43
+ end
44
+ content = prepend_str + content
45
+ match = super content, prefix
46
+ [match, prepend_str.length]
47
+ end
48
+
49
+ def context_ok? content, chunk_start
50
+ preceding_string = content[chunk_start - 2..chunk_start - 1]
51
+ preceding_string !~ /(?:#{REJECTED_PREFIX_RE})$/
52
+ end
53
+ end
54
+
55
+ def interpret match, _content
56
+ chunk = match[0]
57
+ last_char = chunk[-1, 1]
58
+ chunk.gsub!(/(?:&nbsp;)+/, "")
59
+
60
+ @trailing_punctuation =
61
+ if %w[, . ) ! ? :].member?(last_char)
62
+ @text.chop!
63
+ chunk.chop!
64
+ last_char
65
+ end
66
+ chunk.sub!(/\.$/, "")
67
+
68
+ @link_text = chunk
69
+ @uri = ::URI.parse(chunk)
70
+ @process_chunk = process_uri_chunk
71
+ rescue ::URI::Error => e
72
+ # warn "rescue parse #{chunk_class}:
73
+ # '#{m}' #{e.inspect} #{e.backtrace*"\n"}"
74
+ Rails.logger.warn "rescue parse #{self.class}: #{e.inspect}"
75
+ end
76
+
77
+ private
78
+
79
+ def process_text
80
+ @link_text
81
+ end
82
+
83
+ def process_uri_chunk
84
+ link = format.link_to_resource @link_text, process_text
85
+ "#{link}#{@trailing_punctuation}"
86
+ end
87
+ end
88
+
89
+ # FIXME: DRY, merge these two into one class
90
+ class EmailUri < Uri
91
+ PREPEND_STR = "mailto:".freeze
92
+ EMAIL = '[a-zA-Z\\d](?:[-a-zA-Z\\d.]*[a-zA-Z\\d])?\\@'.freeze
93
+
94
+ Card::Content::Chunk.register_class(
95
+ self, prefix_re: "(?:(?!#{REJECTED_PREFIX_RE})#{EMAIL})\\b",
96
+ full_re: /\A#{::URI.regexp(SCHEMES)}/,
97
+ prepend_str: PREPEND_STR,
98
+ idx_char: "@"
99
+ )
100
+
101
+ # removes the prepended string from the unchanged match text
102
+ def process_text
103
+ @text = @text.sub(/^mailto:/, "")
104
+ end
105
+ end
106
+
107
+ class HostUri < Uri
108
+ GENERIC = "aero|biz|com|coop|edu|gov|info|int|mil|" \
109
+ "museum|name|net|org".freeze
110
+
111
+ COUNTRY = "ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|az|ba|bb|bd|be|" \
112
+ "bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cf|cd|cg|" \
113
+ "ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|" \
114
+ "ec|ee|eg|eh|er|es|et|fi|fj|fk|fm|fo|fr|fx|ga|gb|gd|ge|gf|gh|" \
115
+ "gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|" \
116
+ "il|in|io|iq|ir|is|it|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|" \
117
+ "kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mk|ml|mm|" \
118
+ "mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|" \
119
+ "no|np|nr|nt|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|pt|pw|py|" \
120
+ "qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|" \
121
+ "st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tm|tn|to|tp|tr|tt|tv|tw|" \
122
+ "tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|" \
123
+ "za|zm|zr|zw|" \
124
+ "eu".freeze # made this separate, since it's not technically
125
+ # a country -efm
126
+ # These are needed otherwise HOST will match almost anything
127
+
128
+ TLDS = "(?:#{GENERIC}|#{COUNTRY})".freeze
129
+ # TLDS = "(?:#{GENERIC})"
130
+
131
+ PREPEND_STR = "http://".freeze
132
+ HOST = "(?:[a-zA-Z\\d](?:[-a-zA-Z\\d]*[a-zA-Z\\d])?\\.)+#{TLDS}".freeze
133
+
134
+ Card::Content::Chunk.register_class(
135
+ self, prefix_re: "(?:(?!#{REJECTED_PREFIX_RE})#{HOST})\\b",
136
+ full_re: /\A#{::URI.regexp(SCHEMES)}/,
137
+ prepend_str: PREPEND_STR
138
+ )
139
+
140
+ # removes the prepended string from the unchanged match text
141
+ def process_text
142
+ @text = @text.sub(%r{^http://}, "")
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,62 @@
1
+ class Card
2
+ class Content
3
+ module Chunk
4
+ class ViewStub < Abstract
5
+ Chunk.register_class(
6
+ self,
7
+ prefix_re: Regexp.escape("(StUb"),
8
+ full_re: /\A\(StUb(.*?)sTuB\)/m,
9
+ idx_char: "("
10
+ )
11
+
12
+ def initialize text, content
13
+ super
14
+ end
15
+
16
+ def interpret match, _content
17
+ @stub_hash = initial_stub_hash match[1]
18
+ interpret_hash_values
19
+ end
20
+
21
+ def initial_stub_hash string
22
+ JSON.parse(string).symbolize_keys
23
+ # MessagePack.unpack(hex_to_bin(string)).symbolize_keys
24
+ end
25
+
26
+ def hex_to_bin string
27
+ string.scan(/../).map { |x| x.hex.chr }.join
28
+ end
29
+
30
+ def interpret_hash_values
31
+ @stub_hash.keys.each do |key|
32
+ send "interpret_#{key}"
33
+ end
34
+ end
35
+
36
+ def interpret_cast
37
+ @stub_hash[:cast].symbolize_keys!
38
+ end
39
+
40
+ def interpret_view_opts
41
+ @stub_hash[:view_opts].symbolize_keys!
42
+ end
43
+
44
+ def interpret_format_opts
45
+ hash = @stub_hash[:format_opts]
46
+ hash.symbolize_keys!
47
+ hash[:nest_mode] = hash[:nest_mode].to_sym
48
+ hash[:override] = hash[:override] == "true"
49
+ hash[:context_names].map!(&:to_name)
50
+ end
51
+
52
+ def process_chunk
53
+ @processed = format.stub_nest @stub_hash
54
+ end
55
+
56
+ def result
57
+ @processed
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,100 @@
1
+ class << self
2
+
3
+ def included host_class
4
+ track_mod_name host_class, caller
5
+ end
6
+
7
+ def track_mod_name host_class, caller
8
+ host_class.mattr_accessor :file_content_mod_name
9
+ host_class.file_content_mod_name = Card::Set.mod_name(caller)
10
+ end
11
+ end
12
+
13
+ # FIXME: these should abstracted and configured on the types
14
+ # (same codes for `rake card:create:codefile`)
15
+
16
+ # @return [Array<String>, String] the name of file(s) to be loaded
17
+ def source_files
18
+ case type_id
19
+ when CoffeeScriptID then "#{codename}.js.coffee"
20
+ when JavaScriptID then "#{codename}.js"
21
+ when CssID then "#{codename}.css"
22
+ when ScssID then "#{codename}.scss"
23
+ end
24
+ end
25
+
26
+ def source_dir
27
+ case type_id
28
+ when CoffeeScriptID, JavaScriptID then "lib/javascript"
29
+ when CssID, ScssID then "lib/stylesheets"
30
+ else
31
+ "lib"
32
+ end
33
+ end
34
+
35
+ def find_file filename
36
+ File.join(mod_path, source_dir, filename).tap do |file_path|
37
+ return nil if unknown_file? filename, file_path
38
+ end
39
+ end
40
+
41
+ def mod_path
42
+ modname = file_content_mod_name
43
+ if (match = modname.match(/^card-mod-(\w*)/))
44
+ modname = match[1]
45
+ end
46
+ Cardio::Mod.dirs.path modname
47
+ end
48
+
49
+ def unknown_file? filename, file_path
50
+ return false if File.exist? file_path
51
+
52
+ Rails.logger.info "couldn't locate file #{filename} at #{file_path}"
53
+ true
54
+ end
55
+
56
+ def existing_source_paths
57
+ Array.wrap(source_files).map do |filename|
58
+ find_file(filename)
59
+ end.compact
60
+ end
61
+
62
+ def source_changed? since:
63
+ existing_source_paths.any? { |path| ::File.mtime(path) > since }
64
+ end
65
+
66
+ def content
67
+ Array.wrap(source_files).map do |filename|
68
+ if (source_path = find_file filename)
69
+ Rails.logger.debug "reading file: #{source_path}"
70
+ File.read source_path
71
+ end
72
+ end.compact.join "\n"
73
+ end
74
+
75
+ format :html do
76
+ view :input do
77
+ "Content is stored in file and can't be edited."
78
+ end
79
+
80
+ view :file_size do
81
+ "#{card.name}: #{number_to_human_size card.content.bytesize}"
82
+ end
83
+
84
+ def short_content
85
+ fa_icon("exclamation-circle", class: "text-muted pr-2") +
86
+ wrap_with(:span, "file", class: "text-muted")
87
+ end
88
+
89
+ def standard_submit_button
90
+ multi_card_editor? ? super : ""
91
+ end
92
+ end
93
+
94
+ def coffee_files files
95
+ files.map { |f| "script_#{f}.js.coffee" }
96
+ end
97
+
98
+ def scss_files files
99
+ files.map { |f| "style_#{f}.scss" }
100
+ end
@@ -0,0 +1,24 @@
1
+
2
+ def self.included host_class
3
+ host_class.mattr_accessor :template_path
4
+ host_class.extend Card::Set::Format::HamlPaths
5
+ host_class.template_path = host_class.haml_template_path
6
+ end
7
+
8
+ def content
9
+ File.read template_path
10
+ end
11
+
12
+ format :html do
13
+ view :input do
14
+ "Content is managed by code and cannot be edited"
15
+ end
16
+
17
+ def haml_locals
18
+ {}
19
+ end
20
+
21
+ view :core do
22
+ haml card.content, haml_locals
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ def lock
2
+ was_already_locked = locked?
3
+ return if was_already_locked
4
+ Auth.as_bot do
5
+ lock!
6
+ yield
7
+ end
8
+ ensure
9
+ unlock! unless was_already_locked
10
+ end
11
+
12
+ def lock_cache_key
13
+ "UPDATE-LOCK:#{key}"
14
+ end
15
+
16
+ def locked?
17
+ Card.cache.read lock_cache_key
18
+ end
19
+
20
+ def lock!
21
+ Card.cache.write lock_cache_key, true
22
+ end
23
+
24
+ def unlock!
25
+ Card.cache.write lock_cache_key, false
26
+ end
@@ -0,0 +1,7 @@
1
+ format :html do
2
+ view :core do
3
+ with_nest_mode :template do
4
+ super()
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ include_set Abstract::CodeFile
2
+
3
+ def self.included host_class
4
+ Abstract::CodeFile.track_mod_name host_class, caller
5
+ end
6
+
7
+ def source_dir
8
+ "vendor"
9
+ end
@@ -0,0 +1,142 @@
1
+
2
+ def chunks content, type, named=false
3
+ content ||= self.content
4
+ type ||= Card::Content::Chunk
5
+ all_chunks = Card::Content.new(content, self).find_chunks type
6
+ named ? all_chunks.select(&:referee_name) : all_chunks
7
+ end
8
+
9
+ def reference_chunks content=nil, named=true
10
+ chunks content, Card::Content::Chunk::Reference, named
11
+ end
12
+
13
+ # named=true rejects commented nests
14
+ def nest_chunks content=nil, named=true
15
+ chunks content, Card::Content::Chunk::Nest, named
16
+ end
17
+
18
+ # named=true rejects external links (since the don't refer to a card name)
19
+ def link_chunks content=nil, named=false
20
+ chunks content, Card::Content::Chunk::Link, named
21
+ end
22
+
23
+ def each_item_name_with_options content=nil
24
+ reference_chunks(content).each do |chunk|
25
+ options = chunk.respond_to?(:options) ? chunk.options : {}
26
+ yield chunk.referee_name, options
27
+ end
28
+ end
29
+
30
+ format do
31
+ def nest_chunks content=nil
32
+ content ||= _render_raw
33
+ card.nest_chunks content
34
+ end
35
+
36
+ def nested_cards content=nil
37
+ nest_chunks(content).map(&:referee_card).uniq
38
+ end
39
+
40
+ def edit_fields
41
+ voo.edit_structure || []
42
+ end
43
+
44
+ def nested_field_names content=nil
45
+ nest_chunks(content).map(&:referee_name).select { |n| field_name? n }
46
+ end
47
+
48
+ def nested_field_cards content=nil
49
+ nested_cards(content).select { |c| field_name? c.name }
50
+ end
51
+
52
+ def field_name? name
53
+ name.field_of? card.name
54
+ end
55
+
56
+ # @return [Array] of Arrays. each is [nest_name, nest_options_hash]
57
+ def edit_field_configs fields_only=false
58
+ if edit_fields.present?
59
+ explicit_edit_fields_config # explicitly configured in voo or code
60
+ else
61
+ implicit_edit_fields_config fields_only # inferred from nests
62
+ end
63
+ end
64
+
65
+ def implicit_edit_fields_config fields_only
66
+ result = []
67
+ each_nested_chunk(fields: fields_only) do |chunk|
68
+ result << [chunk.options[:nest_name], chunk.options]
69
+ end
70
+ result
71
+ end
72
+
73
+ def each_nested_field_chunk &block
74
+ each_nested_chunk fields: true, &block
75
+ end
76
+
77
+ def each_nested_chunk content: nil, fields: false, uniq: true, virtual: true, &block
78
+ return unless block_given?
79
+ chunks = prepare_nested_chunks content, fields, uniq
80
+ process_nested_chunks chunks, virtual, &block
81
+ end
82
+
83
+ def uniq_chunks chunks
84
+ processed = ::Set.new [card.key]
85
+ chunks.select do |chunk|
86
+ key = chunk.referee_name.key
87
+ ok = !processed.include?(key)
88
+ processed << key
89
+ ok
90
+ end
91
+ end
92
+
93
+ def field_chunks chunks
94
+ chunks.select { |chunk| field_name?(chunk.referee_name) }
95
+ end
96
+
97
+ private
98
+
99
+ def prepare_nested_chunks content, fields, uniq
100
+ chunks = nest_chunks content
101
+ chunks = field_chunks chunks if fields
102
+ chunks = uniq_chunks chunks if uniq
103
+ chunks
104
+ end
105
+
106
+ def process_nested_chunks chunks, virtual, &block
107
+ chunks.each do |chunk|
108
+ process_nested_chunk chunk, virtual, &block
109
+ end
110
+ end
111
+
112
+ def process_nested_chunk chunk, virtual, &block
113
+ if chunk.referee_card&.virtual?
114
+ process_nested_virtual_chunk chunk, &block unless virtual
115
+ else
116
+ yield chunk
117
+ end
118
+ end
119
+
120
+ def process_virtual_chunk chunk
121
+ subformat(chunk.referee_card).each_nested_field_chunk { |sub_chunk| yield sub_chunk }
122
+ end
123
+
124
+ def explicit_edit_fields_config
125
+ edit_fields.map do |cardish, options|
126
+ field_mark = normalized_edit_field_mark cardish, options
127
+ options = normalized_edit_field_options options, Card::Name[field_mark]
128
+ [field_mark, options]
129
+ end
130
+ end
131
+
132
+ def normalized_edit_field_options options, cardname
133
+ options ||= cardname
134
+ options.is_a?(String) ? { title: options } : options
135
+ end
136
+
137
+ def normalized_edit_field_mark cardish, options
138
+ return cardish if cardish.is_a?(Card) ||
139
+ (options.is_a?(Hash) && options.delete(:absolute))
140
+ card.name.field cardish
141
+ end
142
+ end
@@ -0,0 +1,70 @@
1
+
2
+ def is_template?
3
+ return @is_template unless @is_template.nil?
4
+
5
+ @is_template = name.trait_name? :structure, :default
6
+ end
7
+
8
+ def is_structure?
9
+ return @is_structure unless @is_structure.nil?
10
+
11
+ @is_structure = name.trait_name? :structure
12
+ end
13
+
14
+ def template
15
+ # currently applicable templating card.
16
+ # note that a *default template is never returned for an existing card.
17
+ @template ||= begin
18
+ @virtual = false
19
+
20
+ if new_card?
21
+ new_card_template
22
+ else
23
+ structure_rule_card
24
+ end
25
+ end
26
+ end
27
+
28
+ def default_type_id
29
+ Card.default_type_id
30
+ end
31
+
32
+ def new_card_template
33
+ default = rule_card :default, skip_modules: true
34
+ return default unless (structure = dup_structure default&.type_id)
35
+
36
+ @virtual = true if compound?
37
+ self.type_id = structure.type_id if assign_type_to?(structure)
38
+ structure
39
+ end
40
+
41
+ def dup_structure type_id
42
+ dup_card = dup
43
+ dup_card.type_id = type_id || default_type_id
44
+ dup_card.structure_rule_card
45
+ end
46
+
47
+ def assign_type_to? structure
48
+ return if type_id == structure.type_id
49
+ structure.assigns_type?
50
+ end
51
+
52
+ def assigns_type?
53
+ # needed because not all *structure templates govern the type of set members
54
+ # for example, X+*type+*structure governs all cards of type X,
55
+ # but the content rule does not (in fact cannot) have the type X.
56
+ pattern_code = Card.quick_fetch(name.trunk_name.tag_name)&.codename
57
+ return unless pattern_code && (set_class = Set::Pattern.find pattern_code)
58
+
59
+ set_class.assigns_type
60
+ end
61
+
62
+ def structure
63
+ template&.is_structure? ? template : nil
64
+ end
65
+
66
+ def structure_rule_card
67
+ return unless (card = rule_card :structure, skip_modules: true)
68
+
69
+ card.db_content&.strip == "_self" ? nil : card
70
+ end
@@ -0,0 +1,34 @@
1
+ include_set Abstract::TemplatedNests
2
+
3
+ format :html do
4
+ view :one_line_content do
5
+ raw = _render_raw
6
+ "#{card.type_name} : #{raw.present? ? raw : '<em>empty</em>'}"
7
+ end
8
+
9
+ def quick_form_opts
10
+ super.merge "data-update-foreign-slot":
11
+ ".card-slot.quick_edit-view.RIGHT-Xinput_type,"\
12
+ ".card-slot.quick_edit-view.RIGHT-Xcontent_option"\
13
+ ".card-slot.quick_edit-view.RIGHT-Xcontent_option_view"
14
+ end
15
+
16
+ def quick_editor
17
+ wrap_type_formgroup do
18
+ type_field class: "type-field rule-type-field _submit-on-select"
19
+ end +
20
+ wrap_content_formgroup do
21
+ text_field :content, class: "d0-card-content _submit-after-typing"
22
+ end
23
+ end
24
+
25
+ def visible_cardtype_groups
26
+ hash = ::Card::Set::Self::Cardtype::GROUP.slice("Text", "Data", "Upload")
27
+ hash["Organize"] = ["List", "Pointer", "Link list", "Nest list"]
28
+ hash
29
+ end
30
+ end
31
+
32
+ def empty_ok?
33
+ true
34
+ end
@@ -0,0 +1,72 @@
1
+ include_set Abstract::TemplatedNests
2
+
3
+ format :rss do
4
+ def raw_feed_items
5
+ [card]
6
+ end
7
+ end
8
+
9
+ format :html do
10
+ view :one_line_content do
11
+ "#{_render_type} : #{_render_raw}"
12
+ end
13
+
14
+ def visible_cardtype_groups
15
+ hash = ::Card::Set::Self::Cardtype::GROUP.slice("Text")
16
+ hash["Organize"] = ["Search", "Nest list"]
17
+ hash
18
+ end
19
+ end
20
+
21
+ event :update_structurees_references, :integrate,
22
+ when: :update_structurees_references? do
23
+ return unless (query = structuree_query)
24
+
25
+ Auth.as_bot do
26
+ query.run.each(&:update_references_out)
27
+ end
28
+ end
29
+
30
+ def update_structurees_references?
31
+ db_content_changed? || action == :delete
32
+ end
33
+
34
+ event :reset_cache_to_use_new_structure,
35
+ before: :update_structurees_references do
36
+ Card::Cache.reset_hard
37
+ Card::Cache.reset_soft
38
+ end
39
+
40
+ event :update_structurees_type, :finalize,
41
+ changed: :type_id, when: proc { |c| c.assigns_type? } do
42
+ update_structurees type_id: type_id
43
+ end
44
+
45
+ def structuree_names
46
+ return [] unless (query = structuree_query(return: :name))
47
+
48
+ Auth.as_bot do
49
+ query.run
50
+ end
51
+ end
52
+
53
+ def update_structurees args
54
+ # note that this is not smart about overriding templating rules
55
+ # for example, if someone were to change the type of a
56
+ # +*right+*structure rule that was overridden
57
+ # by a +*type plus right+*structure rule, the override would not be respected.
58
+ return unless (query = structuree_query(return: :id))
59
+
60
+ Auth.as_bot do
61
+ query.run.each_slice(100) do |id_batch|
62
+ Card.where(id: id_batch).update_all args
63
+ end
64
+ end
65
+ end
66
+
67
+ def structuree_query args={}
68
+ set_card = trunk
69
+ return unless set_card.type_id == SetID
70
+
71
+ set_card.fetch_query args
72
+ end
@@ -0,0 +1,2 @@
1
+ setting_opts group: :templating, position: 2, rule_type_editable: true,
2
+ short_help_text: "type/content template for new cards"
@@ -0,0 +1,4 @@
1
+ setting_opts group: :templating, position: 2, rule_type_editable: true,
2
+ short_help_text: "control card's content / structure",
3
+ help_text: "Controls cards' content / structure. "\
4
+ "[[http://decko.org/formatting|more]]"
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: card-mod-content
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.11.0
5
+ platform: ruby
6
+ authors:
7
+ - Ethan McCutchen
8
+ - Philipp Kühl
9
+ - Gerry Gleason
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2020-12-24 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: card
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.101.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - '='
27
+ - !ruby/object:Gem::Version
28
+ version: 1.101.0
29
+ description: ''
30
+ email:
31
+ - info@decko.org
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - lib/card/content/chunk/escaped_literal.rb
37
+ - lib/card/content/chunk/keep_escaped_literal.rb
38
+ - lib/card/content/chunk/link.rb
39
+ - lib/card/content/chunk/nest.rb
40
+ - lib/card/content/chunk/query_reference.rb
41
+ - lib/card/content/chunk/reference.rb
42
+ - lib/card/content/chunk/uri.rb
43
+ - lib/card/content/chunk/view_stub.rb
44
+ - set/abstract/code_file.rb
45
+ - set/abstract/haml_file.rb
46
+ - set/abstract/lock.rb
47
+ - set/abstract/templated_nests.rb
48
+ - set/abstract/vendor_code_file.rb
49
+ - set/all/chunk.rb
50
+ - set/all/templating.rb
51
+ - set/right/default.rb
52
+ - set/right/structure.rb
53
+ - set/self/default.rb
54
+ - set/self/structure.rb
55
+ homepage: http://decko.org
56
+ licenses:
57
+ - GPL-3.0
58
+ metadata:
59
+ card-mod: content
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '2.5'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.0.3
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: card content handling
79
+ test_files: []