hashtags 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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