card-mod-content 0.11.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,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: []