hashtags 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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
File without changes
@@ -0,0 +1,5 @@
1
+ en:
2
+ hashtags/builders:
3
+ help:
4
+ type: type
5
+ for: for
@@ -0,0 +1,5 @@
1
+ Hashtags::Engine.routes.draw do
2
+ namespace :hashtags do
3
+ get 'resources' => 'resources#index', as: :resources
4
+ end
5
+ end
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hashtags/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'hashtags'
8
+ spec.version = Hashtags::VERSION
9
+ spec.authors = ['Tomas Celizna']
10
+ spec.email = ['tomas.celizna@gmail.com']
11
+
12
+ spec.summary = 'Rails engine to facilitate inline text hashtags.'
13
+ spec.homepage = 'https://github.com/tomasc/hashtags'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'handlebars'
22
+ spec.add_dependency 'rails', '~> 4'
23
+ spec.add_dependency 'rails-assets-jquery-textcomplete'
24
+ spec.add_dependency 'rails-assets-handlebars'
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.12'
27
+ spec.add_development_dependency 'guard'
28
+ spec.add_development_dependency 'guard-minitest'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
30
+ spec.add_development_dependency 'minitest', '~> 5.0'
31
+
32
+ spec.add_development_dependency 'database_cleaner'
33
+ spec.add_development_dependency 'mongoid', '~> 5.0'
34
+ spec.add_development_dependency 'slim'
35
+ end
@@ -0,0 +1,3 @@
1
+ //= require jquery-textcomplete
2
+ //= require handlebars
3
+ //= require hashtags/hashtags
@@ -0,0 +1,66 @@
1
+ #= require handlebars
2
+ #= require jquery-textcomplete
3
+
4
+ # https://github.com/zenorocha/jquery-boilerplate
5
+ (($, window) ->
6
+ pluginName = 'hashtags'
7
+ document = window.document
8
+
9
+ defaults =
10
+ debug: false
11
+
12
+ class Plugin
13
+ constructor: (@element, options) ->
14
+ @options = $.extend {}, defaults, options
15
+
16
+ @$element = $(@element)
17
+
18
+ @_defaults = defaults
19
+ @_name = pluginName
20
+
21
+ @init()
22
+
23
+ init: ->
24
+ @$element.textcomplete(@get_strategies())
25
+
26
+ get_data: -> @$element.data 'hashtags'
27
+ get_default_path: -> @get_data()['path']
28
+ get_strategies: -> $.map @get_data()['strategies'], (strategy) => @get_strategy(strategy)
29
+ get_strategy: (data) ->
30
+ {
31
+ index: data.match_index
32
+ cache: true
33
+ context: (text) -> text.toLowerCase()
34
+ match: ///#{data.match_regexp}///
35
+ search: (term, callback, match) =>
36
+ if data.values then @search_values(data, term, callback, match)
37
+ else @search_remote(data, term, callback, match)
38
+ replace: (resource, event) -> Handlebars.compile(data.replace)(resource)
39
+ template: (resource, term) -> Handlebars.compile(data.template)(resource)
40
+ }
41
+
42
+ search_values: (data, term, callback, match) ->
43
+ callback $.map( data.values, (value) ->
44
+ if value.match(///^#{term}///i) then value else null
45
+ )
46
+
47
+ search_remote: (data, term, callback, match) ->
48
+ url = data.path ? @get_default_path()
49
+ match_template = data.match_template
50
+
51
+ $.getJSON( url , q: term, class_name: data.class_name )
52
+ .done( (resp) ->
53
+ callback $.map( resp, (resource) ->
54
+ if Handlebars.compile(match_template)(resource).match(///#{term}///i) then resource else null
55
+ )
56
+ )
57
+ .fail -> callback []
58
+
59
+ $.fn[pluginName] = (options) ->
60
+ @each ->
61
+ if !$.data(this, "plugin_#{pluginName}")
62
+ $.data(@, "plugin_#{pluginName}", new Plugin(@, options))
63
+
64
+ )(jQuery, window)
65
+
66
+ $ -> $('input[type="text"][data-hashtags], textarea[data-hashtags]').hashtags()
@@ -0,0 +1,3 @@
1
+ /*
2
+ *= require_tree .
3
+ */
@@ -0,0 +1,11 @@
1
+ .hashtags.help {
2
+ .trigger_group {
3
+ &:not(:last-child):after {
4
+ content: '; ';
5
+ }
6
+
7
+ .item:not(:last-child):after {
8
+ content: ', ';
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,21 @@
1
+ ul.textcomplete-dropdown {
2
+ background-color: white;
3
+ border: 1px solid;
4
+ list-style: none;
5
+ margin: 0;
6
+ padding: 0;
7
+
8
+ li.textcomplete-item {
9
+ border-radius: 0;
10
+ margin: 0;
11
+ padding: .25em .5em;
12
+ &:not(:last-child) {
13
+ border-bottom: 1px solid;
14
+ }
15
+
16
+ &:hover, &.active {
17
+ cursor: pointer;
18
+ background-color: yellow;
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,11 @@
1
+ require 'hashtags/base'
2
+ require 'hashtags/builder'
3
+
4
+ require 'hashtags/mongoid_extension'
5
+ require 'hashtags/resource_type'
6
+ require 'hashtags/resource'
7
+ require 'hashtags/user'
8
+ require 'hashtags/variable'
9
+
10
+ require 'hashtags/engine'
11
+ require 'hashtags/version'
@@ -0,0 +1,161 @@
1
+ module Hashtags
2
+ # Base class, from which all Hashtag types inherit
3
+ # In case you wish to add a new type next to
4
+ # • ResourceType
5
+ # • Resource
6
+ # • User
7
+ # • Variable
8
+ # you might want to inherit from Base.
9
+
10
+ class Base < Struct.new(:str)
11
+ def self.to_markup(str)
12
+ new(str).to_markup
13
+ end
14
+
15
+ def self.to_hashtag(str)
16
+ new(str).to_hashtag
17
+ end
18
+
19
+ def self.descendants
20
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
21
+ end
22
+
23
+ def self.resource_classes
24
+ Resource.descendants
25
+ end
26
+
27
+ def self.user_classes
28
+ User.descendants
29
+ end
30
+
31
+ def self.variable_classes
32
+ Variable.descendants
33
+ end
34
+
35
+ # to be passed to textcomplete
36
+ def self.strategy(hashtag_classes)
37
+ {
38
+ class_name: to_s,
39
+ match_regexp: json_regexp(match_regexp),
40
+ match_index: match_index,
41
+ match_template: match_template,
42
+ path: path,
43
+ replace: replace,
44
+ template: template,
45
+ values: values(hashtag_classes)
46
+ }
47
+ end
48
+
49
+ # for example @ # $ …
50
+ def self.trigger
51
+ raise NotImplementedError
52
+ end
53
+
54
+ def self.match_template
55
+ end
56
+
57
+ # used to expire field with tags
58
+ def self.cache_key
59
+ raise NotImplementedError
60
+ end
61
+
62
+ # regexp to recognize the complete tag
63
+ def self.regexp
64
+ raise NotImplementedError
65
+ end
66
+
67
+ # implement to use custom controller
68
+ def self.path
69
+ end
70
+
71
+ # implement to preload hash tag values,
72
+ # fe in case there is limited number (ie variable names)
73
+ def self.values(hashtag_classes)
74
+ end
75
+
76
+ # implement to show which values particular trigger offers
77
+ # fe # -> lists all available resource types
78
+ def self.help_values
79
+ end
80
+
81
+ # ---------------------------------------------------------------------
82
+ # JS
83
+
84
+ # trigger dropdown when user input matches this regexp
85
+ def self.match_regexp
86
+ raise NotImplementedError
87
+ end
88
+
89
+ # index of the match group from the above regexp to be used for matching
90
+ def self.match_index
91
+ raise NotImplementedError
92
+ end
93
+
94
+ # tag that gets inserted into the field
95
+ def self.replace
96
+ raise NotImplementedError
97
+ end
98
+
99
+ # fragment to be displayed in the dropdown menu
100
+ def self.template
101
+ raise NotImplementedError
102
+ end
103
+
104
+ # ---------------------------------------------------------------------
105
+
106
+ # return JSON version of resources that match query
107
+ # this is returned when user starts typing (the query)
108
+ def self.json_for_query(query)
109
+ raise NotImplementedError
110
+ end
111
+
112
+ def self.values(hashtag_classes = Variable.descendants)
113
+ end
114
+
115
+ # ---------------------------------------------------------------------
116
+
117
+ # converts Ruby tegexp to JS regexp
118
+ def self.json_regexp(regexp)
119
+ str = regexp.inspect
120
+ .sub('\\A', '^')
121
+ .sub('\\Z', '$')
122
+ .sub('\\z', '$')
123
+ .sub(/^\//, '')
124
+ .sub(/\/[a-z]*$/, '')
125
+ .gsub(/\(\?#.+\)/, '')
126
+ .gsub(/\(\?-\w+:/, '(')
127
+ .gsub(/\s/, '')
128
+ Regexp.new(str).source
129
+ end
130
+
131
+ # ---------------------------------------------------------------------
132
+
133
+ # Converts hash tags to markup
134
+ def to_markup
135
+ str.to_s.gsub(self.class.regexp) do |match|
136
+ m = markup(Regexp.last_match)
137
+ match = m || match
138
+ end
139
+ end
140
+
141
+ # Updates hash tags
142
+ def to_hashtag
143
+ str.to_s.gsub(self.class.regexp) do |match|
144
+ ht = hashtag(Regexp.last_match)
145
+ match = ht || match
146
+ end
147
+ end
148
+
149
+ # ---------------------------------------------------------------------
150
+
151
+ # the proper hashtag (so it can be updated automatically)
152
+ def hashtag(match)
153
+ raise NotImplementedError
154
+ end
155
+
156
+ # what is the hashtag replaced with in the end
157
+ def markup(match)
158
+ raise NotImplementedError
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,81 @@
1
+ module Hashtags
2
+ class Builder < Struct.new(:options)
3
+ def self.to_markup(str, options = {})
4
+ new(options).to_markup(str)
5
+ end
6
+
7
+ def self.to_hashtag(str, options = {})
8
+ new(options).to_hashtag(str)
9
+ end
10
+
11
+ def self.dom_data(options = {})
12
+ new(options).dom_data
13
+ end
14
+
15
+ def self.help(options = {})
16
+ new(options).help
17
+ end
18
+
19
+ def initialize(options = {})
20
+ super(options)
21
+ end
22
+
23
+ # collects markup from all hashtags classes
24
+ def to_markup(str)
25
+ hashtag_classes.inject(str) { |res, cls| cls.to_markup(res) }
26
+ end
27
+
28
+ # collects hashtags from all hashtags classes
29
+ def to_hashtag(str)
30
+ hashtag_classes.inject(str) { |res, cls| cls.to_hashtag(res) }
31
+ end
32
+
33
+ # render textcomplete dom data
34
+ def dom_data
35
+ { hashtags: {
36
+ path: Engine.routes.url_helpers.hashtags_resources_path,
37
+ strategies: hashtag_strategies
38
+ } }
39
+ end
40
+
41
+ # render help string
42
+ def help
43
+ hashtag_classes.group_by(&:trigger).map do |trigger, cls|
44
+ OpenStruct.new(hashtag_classes: cls, trigger: trigger, help_values: cls.map(&:help_values).flatten.compact.sort)
45
+ end
46
+ end
47
+
48
+ def filter_classes(cls)
49
+ return [] unless cls.present?
50
+ res = cls
51
+ res &= options[:only] if options[:only]
52
+ res -= options[:except] if options[:except]
53
+ res
54
+ end
55
+
56
+ private
57
+
58
+ def hashtag_classes
59
+ filter_classes(
60
+ Resource.resource_classes +
61
+ User.user_classes +
62
+ Variable.variable_classes
63
+ )
64
+ end
65
+
66
+ def hashtag_strategies
67
+ cls = hashtag_classes.dup
68
+
69
+ # add resource type strategy if needed
70
+ cls << ResourceType if cls.any? { |c| c < Resource } && !cls.include?(ResourceType)
71
+
72
+ # remove all variable classes and replace them with one strategy
73
+ if variable_cls = cls.select { |c| c < Variable } && variable_cls.present?
74
+ cls -= variable_cls
75
+ cls << Variable
76
+ end
77
+
78
+ cls.collect { |c| c.strategy(hashtag_classes) }
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,7 @@
1
+ module Hashtags
2
+ class Engine < ::Rails::Engine
3
+ engine_name = :hashtags
4
+
5
+ I18n.load_path += Dir[Engine.root.join('config', 'locales', '**', '*.{rb,yml}')]
6
+ end
7
+ end
@@ -0,0 +1,36 @@
1
+ require 'mongoid'
2
+ require 'ostruct'
3
+
4
+ Mongoid::Fields.option :hashtags do |cls, field, value|
5
+ return if value == false
6
+
7
+ cls.define_singleton_method(:hashtags) { @hashtags ||= {} } unless cls.respond_to?(:hashtags)
8
+ options = value.is_a?(Hash) ? value.slice(*%i(only except)) : {}
9
+
10
+ cls.hashtags[field.name].define_singleton_method :dom_data do
11
+ Hashtags::Builder.dom_data(options)
12
+ end
13
+
14
+ cls.hashtags[field.name].define_singleton_method :help do
15
+ Hashtags::Builder.help(options)
16
+ end
17
+
18
+ cls.hashtags[field.name].define_singleton_method :options do
19
+ options
20
+ end
21
+
22
+ field.define_singleton_method :demongoize do |*args|
23
+ res = super(*args)
24
+ res.define_singleton_method :to_markup do
25
+ Hashtags::Builder.to_markup(res.to_s, options).html_safe
26
+ end
27
+ res.define_singleton_method :to_hashtag do
28
+ Hashtags::Builder.to_hashtag(res.to_s, options).html_safe
29
+ end
30
+ res
31
+ end
32
+
33
+ field.define_singleton_method :mongoize do |value|
34
+ Hashtags::Builder.to_hashtag(super(value.to_s), options)
35
+ end
36
+ end