songbooks 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +29 -0
  7. data/LICENSE +22 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +41 -0
  10. data/Rakefile +6 -0
  11. data/app/assets/javascripts/application.coffee +4 -0
  12. data/app/assets/javascripts/components/filters/convertWhitespace.filter.coffee +3 -0
  13. data/app/assets/javascripts/components/song_component/chord.directive.coffee +9 -0
  14. data/app/assets/javascripts/components/song_component/literal.directive.coffee +6 -0
  15. data/app/assets/javascripts/components/song_component/song_component.directive.coffee +6 -0
  16. data/app/assets/javascripts/components/songbook/songbook.controller.coffee +29 -0
  17. data/app/assets/javascripts/components/songs/song.controller.coffee +10 -0
  18. data/app/assets/javascripts/components/songs/song.directive.coffee +7 -0
  19. data/app/assets/javascripts/components/songs/song.service.coffee +98 -0
  20. data/app/assets/javascripts/components/songs/songs.controller.coffee +21 -0
  21. data/app/assets/javascripts/routes.coffee +11 -0
  22. data/app/assets/stylesheets/application.scss +34 -0
  23. data/app/fonts/DejaVuSans-Bold.ttf +0 -0
  24. data/app/fonts/DejaVuSans-BoldOblique.ttf +0 -0
  25. data/app/fonts/DejaVuSans-ExtraLight.ttf +0 -0
  26. data/app/fonts/DejaVuSans-Oblique.ttf +0 -0
  27. data/app/fonts/DejaVuSans.ttf +0 -0
  28. data/app/fonts/DejaVuSansCondensed-Bold.ttf +0 -0
  29. data/app/fonts/DejaVuSansCondensed-BoldOblique.ttf +0 -0
  30. data/app/fonts/DejaVuSansCondensed-Oblique.ttf +0 -0
  31. data/app/fonts/DejaVuSansCondensed.ttf +0 -0
  32. data/app/fonts/DejaVuSansMono-Bold.ttf +0 -0
  33. data/app/fonts/DejaVuSansMono-BoldOblique.ttf +0 -0
  34. data/app/fonts/DejaVuSansMono-Oblique.ttf +0 -0
  35. data/app/fonts/DejaVuSansMono.ttf +0 -0
  36. data/app/fonts/DejaVuSerif-Bold.ttf +0 -0
  37. data/app/fonts/DejaVuSerif-BoldItalic.ttf +0 -0
  38. data/app/fonts/DejaVuSerif-Italic.ttf +0 -0
  39. data/app/fonts/DejaVuSerif.ttf +0 -0
  40. data/app/fonts/DejaVuSerifCondensed-Bold.ttf +0 -0
  41. data/app/fonts/DejaVuSerifCondensed-BoldItalic.ttf +0 -0
  42. data/app/fonts/DejaVuSerifCondensed-Italic.ttf +0 -0
  43. data/app/fonts/DejaVuSerifCondensed.ttf +0 -0
  44. data/app/tex/songbook.tex +12 -0
  45. data/app/views/application.haml +54 -0
  46. data/app/views/layouts/application.haml +40 -0
  47. data/app/views/songbook.prawn +80 -0
  48. data/app/views/songs/component.haml +2 -0
  49. data/app/views/songs/index.jbuilder +5 -0
  50. data/app/views/songs/show.haml +23 -0
  51. data/app/views/songs/show.jbuilder +24 -0
  52. data/app/views/songs/song.haml +4 -0
  53. data/exe/sb_server +6 -0
  54. data/lib/prawn_ext/font/afm.rb +20 -0
  55. data/lib/songbooks.rb +32 -0
  56. data/lib/songbooks/CLI.rb +13 -0
  57. data/lib/songbooks/components/chord.rb +98 -0
  58. data/lib/songbooks/components/literal.rb +57 -0
  59. data/lib/songbooks/components/metadata.rb +69 -0
  60. data/lib/songbooks/components/section.rb +73 -0
  61. data/lib/songbooks/controllers/controller.rb +18 -0
  62. data/lib/songbooks/controllers/songs_controller.rb +30 -0
  63. data/lib/songbooks/folder.rb +32 -0
  64. data/lib/songbooks/server.rb +65 -0
  65. data/lib/songbooks/song.rb +53 -0
  66. data/lib/songbooks/version.rb +3 -0
  67. data/lib/songbooks/view_helper.rb +9 -0
  68. data/public/javascripts/angular-local-storage.js +449 -0
  69. data/public/javascripts/angular-route.js +991 -0
  70. data/public/javascripts/angular.js +28904 -0
  71. data/public/javascripts/application.js +8 -0
  72. data/public/javascripts/components/filters/convertWhitespace.filter.js +8 -0
  73. data/public/javascripts/components/song_component/chord.directive.js +13 -0
  74. data/public/javascripts/components/song_component/literal.directive.js +9 -0
  75. data/public/javascripts/components/song_component/song_component.directive.js +9 -0
  76. data/public/javascripts/components/songbook/songbook.controller.js +33 -0
  77. data/public/javascripts/components/songs/song.controller.js +12 -0
  78. data/public/javascripts/components/songs/song.directive.js +9 -0
  79. data/public/javascripts/components/songs/song.service.js +107 -0
  80. data/public/javascripts/components/songs/songs.controller.js +22 -0
  81. data/public/javascripts/jquery-2.1.4.min.js +4 -0
  82. data/public/javascripts/routes.js +16 -0
  83. data/public/stylesheets/application.css +24 -0
  84. data/public/stylesheets/bootstrap-theme.min.css +5 -0
  85. data/public/stylesheets/bootstrap.min.css +5 -0
  86. data/songbooks.gemspec +51 -0
  87. metadata +369 -0
@@ -0,0 +1,57 @@
1
+ require_relative 'chord'
2
+
3
+ module Songbooks
4
+ module Components
5
+ class Literal
6
+
7
+ #----------------------------------------------------------------
8
+ # Parsing
9
+ #----------------------------------------------------------------
10
+
11
+ #
12
+ # Tries to munch a literal from the given string.
13
+ # If the string starts with a valid literal, it is automatically
14
+ # removed from the string
15
+ #
16
+ # @return [Songbooks::Components::Literal, NilClass] a Literal containing the munched
17
+ # text or +nil+ if the string didn't start with a literal
18
+ #
19
+ def self.munch_literal(text)
20
+ result = ''
21
+
22
+ until text.length.zero?
23
+ # Munch leading whitespace
24
+ if text =~ /^(\s+)/
25
+ result << text.slice!(0...$1.length)
26
+ end
27
+
28
+ # If we reached a chord, we may not munch any further
29
+ break if Chord.starts_with_chord?(text)
30
+
31
+ # Munch non-chord, non whitespace characters
32
+ if text =~/^([^\s+])/
33
+ result << text.slice!(0...$1.length)
34
+ end
35
+ end
36
+
37
+ result == '' ? nil : Literal.new(result)
38
+ end
39
+
40
+ def initialize(string)
41
+ @string = string
42
+ end
43
+
44
+ def to_s
45
+ @string
46
+ end
47
+
48
+ def to_str
49
+ @string
50
+ end
51
+
52
+ def html_string
53
+ @string
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,69 @@
1
+ module Songbooks
2
+ module Components
3
+ class Metadata
4
+
5
+ #----------------------------------------------------------------
6
+ # CONSTANTS
7
+ #----------------------------------------------------------------
8
+
9
+ EMPTY_LINE_REGEXP = /\A\s*\r?\n/
10
+
11
+ REGEXPS = {
12
+ :title => /\A@title\s*\r?\n?([^\n\r]+)/,
13
+ :artist => /\A@artist\s*\r?\n?([^\n\r]+)/,
14
+ }
15
+
16
+ #----------------------------------------------------------------
17
+ # Parsing
18
+ #----------------------------------------------------------------
19
+
20
+ #
21
+ # Tries to parse metadata from the header of a given string.
22
+ # It automatically deletes empty / whitespace lines and the parsed
23
+ # content from the given string.
24
+ #
25
+ # Currently supported metadata/annotations
26
+ # @artist
27
+ # @title
28
+ #
29
+ # @param [String] text
30
+ # The string which possibly contains header data, usually a whole song file content
31
+ #
32
+ # @return [Songbooks::Components::Metadata]
33
+ # The parsed metadata with only the options set that could be parsed.
34
+ #
35
+ def self.munch_header(text)
36
+ found = true
37
+ options = {}
38
+
39
+ while found
40
+ found = false
41
+
42
+ if (match = text.match(EMPTY_LINE_REGEXP))
43
+ text.slice!(0...match[0].length)
44
+ found = true
45
+ else
46
+ REGEXPS.each do |name, regexp|
47
+ if (match = text.match(regexp))
48
+ options[name] = match[1]
49
+ text.slice!(0...match[0].length)
50
+ found = true
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ Metadata.new(options)
57
+ end
58
+
59
+ def initialize(options = {})
60
+ options.each do |k, v|
61
+ instance_variable_set("@#{k}", v)
62
+ end
63
+ end
64
+
65
+ attr_reader :artist, :title
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,73 @@
1
+ require 'songbooks/components/chord'
2
+ require 'songbooks/components/literal'
3
+
4
+ module Songbooks
5
+ module Components
6
+ class Section
7
+
8
+ #----------------------------------------------------------------
9
+ # CONSTANTS
10
+ #----------------------------------------------------------------
11
+
12
+ REGEXP = /^##\s*(.*)$/
13
+
14
+ #----------------------------------------------------------------
15
+ # Parsing
16
+ #----------------------------------------------------------------
17
+
18
+ #
19
+ # Tries to parse the given text line as a section name identifier
20
+ #
21
+ # @return [Hash, NilClass] the parsed information or +nil+ if the given row
22
+ # is not a valid section row
23
+ #
24
+ def self.parse_information(content)
25
+ if content =~ REGEXP
26
+ return {name: $1}
27
+ end
28
+ nil
29
+ end
30
+
31
+ def initialize(name, initial = false)
32
+ @name = name
33
+ @content_lines = []
34
+ @initial = initial
35
+ end
36
+
37
+ #
38
+ # Parses the given line (chords and literals) and adds it to this section
39
+ #
40
+ def parse_line!(_line)
41
+ line = _line.dup.rstrip
42
+ line_components = []
43
+
44
+ until line.length.zero?
45
+ component = Songbooks::Components::Chord.munch_chord(line)
46
+ component ||= Songbooks::Components::Literal.munch_literal(line)
47
+ line_components << component
48
+ end
49
+
50
+ add_line(line_components)
51
+ end
52
+
53
+ def add_line(line)
54
+ @content_lines << line
55
+ end
56
+
57
+ def name
58
+ @name == 'Initial' ? '' : @name
59
+ end
60
+
61
+ def lines
62
+ @content_lines
63
+ end
64
+
65
+ def transpose!(amount)
66
+ lines.flatten.each do |component|
67
+ next unless component.is_a?(Songbooks::Components::Chord)
68
+ component.transpose!(amount)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,18 @@
1
+ module Songbooks
2
+ module Controllers
3
+ class Controller
4
+
5
+ def template
6
+ @template.to_sym
7
+ end
8
+
9
+ def layout
10
+ (@layout || 'layouts/application').to_sym
11
+ end
12
+
13
+ def resulting_instance_variables
14
+ instance_variables - [:@layout, :@params, :@template]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ require 'songbooks/controllers/controller'
2
+
3
+ module Songbooks
4
+ module Controllers
5
+ class SongsController < Controller
6
+
7
+ def initialize(params = {}, session = {})
8
+ @params = params
9
+ @session = session
10
+ end
11
+
12
+ attr_reader :session, :params
13
+
14
+ def index
15
+ @songs = Songbooks.folder.songs
16
+ @template = 'songs/index'
17
+ end
18
+
19
+ def show
20
+ @song = Songbooks.folder.song(params['identifier'])
21
+ @template = 'songs/show'
22
+ end
23
+
24
+ def generate
25
+ @songs = @session['songbook'].map { |id| Songbooks.folder.song(id) }
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,32 @@
1
+ require 'songbooks/song'
2
+
3
+ module Songbooks
4
+ class Folder
5
+ def initialize(path)
6
+ @path = Pathname.new(path)
7
+ end
8
+
9
+ attr_reader :path
10
+
11
+ def song(identifier)
12
+ songs_by_identifier[identifier]
13
+ end
14
+
15
+ def songs
16
+ songs_by_identifier.values
17
+ end
18
+
19
+ private
20
+
21
+ def songs_by_identifier
22
+ @songs ||= chords_files.each_with_object({}) do |file_path, h|
23
+ song = Songbooks::Song.new(File.read(file_path))
24
+ h[song.identifier] = song
25
+ end
26
+ end
27
+
28
+ def chords_files
29
+ Dir[@path.join('**', '*.txt')]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,65 @@
1
+ require 'sinatra/base'
2
+ require 'songbooks/controllers/songs_controller'
3
+ require 'songbooks/view_helper'
4
+
5
+ module Songbooks
6
+ class Server < Sinatra::Base
7
+ include Sinatra::Prawn::Helpers
8
+
9
+ configure do
10
+ enable :sessions
11
+
12
+ set :views, Songbooks.root.join('app', 'views').to_s
13
+ set :public_folder, Songbooks.root.join('public').to_s
14
+
15
+ set :prawn, { :page_layout => :portrait, :page_size => 'A4' }
16
+ end
17
+
18
+ get '/' do
19
+ haml :application
20
+ end
21
+
22
+ get '/songs.json' do
23
+ controller = Songbooks::Controllers::SongsController.new(params)
24
+ controller.index
25
+
26
+ controller.resulting_instance_variables.each do |var_name|
27
+ instance_variable_set(var_name, controller.instance_variable_get(var_name))
28
+ end
29
+
30
+ jbuilder controller.template
31
+ end
32
+
33
+ get '/songs/:identifier.json' do
34
+ controller = Songbooks::Controllers::SongsController.new(params)
35
+ controller.show
36
+
37
+ controller.resulting_instance_variables.each do |var_name|
38
+ instance_variable_set(var_name, controller.instance_variable_get(var_name))
39
+ end
40
+
41
+ jbuilder controller.template
42
+ end
43
+
44
+ post '/songbook' do
45
+ @params = JSON.parse(request.body.read)
46
+ session[:songbook] = params['songs']
47
+ end
48
+
49
+ get '/songbook.pdf' do
50
+ controller = Songbooks::Controllers::SongsController.new(params, session)
51
+ controller.generate
52
+
53
+ controller.resulting_instance_variables.each do |var_name|
54
+ instance_variable_set(var_name, controller.instance_variable_get(var_name))
55
+ end
56
+
57
+ content_type 'application/pdf'
58
+ prawn :songbook
59
+ end
60
+
61
+ get '/static/*' do
62
+ haml params['splat'].first.to_sym
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,53 @@
1
+ require 'songbooks/components/section'
2
+ require 'songbooks/components/metadata'
3
+
4
+ module Songbooks
5
+ class Song
6
+ attr_reader :artist, :title, :identifier
7
+
8
+ def initialize(content)
9
+ parse_content(content)
10
+ end
11
+
12
+ def identifier
13
+ @identifier ||= [artist, title].join('-').gsub(/[^a-zA-Z\-_0-9]/, '_')
14
+ end
15
+
16
+ def sections
17
+ @sections ||= []
18
+ end
19
+
20
+ def longest_section_caption
21
+ sections.map(&:name).sort_by(&:length).last
22
+ end
23
+
24
+ private
25
+
26
+ def parse_content(content)
27
+ current_section = Songbooks::Components::Section.new('Initial', true)
28
+
29
+ meta = Songbooks::Components::Metadata.munch_header(content)
30
+ @artist, @title = meta.artist, meta.title
31
+
32
+ content.lines.each do |line|
33
+ if (information = Songbooks::Components::Section.parse_information(line))
34
+ self.sections << current_section
35
+ current_section = Songbooks::Components::Section.new(information[:name])
36
+ else
37
+ current_section.parse_line!(line)
38
+ end
39
+ end
40
+
41
+ self.sections << current_section
42
+ end
43
+
44
+ def transpose!(count)
45
+ return if count == 0
46
+ sections.each { |section| section.transpose!(count) }
47
+ end
48
+
49
+ def transpose(count)
50
+ dup.transpose!(count)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ module Songbooks
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,9 @@
1
+ module Songbooks
2
+ module ViewHelper
3
+ def self.javascripts
4
+ Dir[Songbooks.root.join('public', 'javascripts', 'components', '**', '*.js').to_s].map do |filename|
5
+ filename.sub(Songbooks.root.join('public', 'javascripts/').to_s, '')
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,449 @@
1
+ /**
2
+ * An Angular module that gives you access to the browsers local storage
3
+ * @version v0.2.3 - 2015-10-11
4
+ * @link https://github.com/grevory/angular-local-storage
5
+ * @author grevory <greg@gregpike.ca>
6
+ * @license MIT License, http://www.opensource.org/licenses/MIT
7
+ */
8
+ (function ( window, angular, undefined ) {
9
+ /*jshint globalstrict:true*/
10
+ 'use strict';
11
+
12
+ var isDefined = angular.isDefined,
13
+ isUndefined = angular.isUndefined,
14
+ isNumber = angular.isNumber,
15
+ isObject = angular.isObject,
16
+ isArray = angular.isArray,
17
+ extend = angular.extend,
18
+ toJson = angular.toJson;
19
+ var angularLocalStorage = angular.module('LocalStorageModule', []);
20
+
21
+ angularLocalStorage.provider('localStorageService', function() {
22
+
23
+ // You should set a prefix to avoid overwriting any local storage variables from the rest of your app
24
+ // e.g. localStorageServiceProvider.setPrefix('yourAppName');
25
+ // With provider you can use config as this:
26
+ // myApp.config(function (localStorageServiceProvider) {
27
+ // localStorageServiceProvider.prefix = 'yourAppName';
28
+ // });
29
+ this.prefix = 'ls';
30
+
31
+ // You could change web storage type localstorage or sessionStorage
32
+ this.storageType = 'localStorage';
33
+
34
+ // Cookie options (usually in case of fallback)
35
+ // expiry = Number of days before cookies expire // 0 = Does not expire
36
+ // path = The web path the cookie represents
37
+ this.cookie = {
38
+ expiry: 30,
39
+ path: '/'
40
+ };
41
+
42
+ // Send signals for each of the following actions?
43
+ this.notify = {
44
+ setItem: true,
45
+ removeItem: false
46
+ };
47
+
48
+ // Setter for the prefix
49
+ this.setPrefix = function(prefix) {
50
+ this.prefix = prefix;
51
+ return this;
52
+ };
53
+
54
+ // Setter for the storageType
55
+ this.setStorageType = function(storageType) {
56
+ this.storageType = storageType;
57
+ return this;
58
+ };
59
+
60
+ // Setter for cookie config
61
+ this.setStorageCookie = function(exp, path) {
62
+ this.cookie.expiry = exp;
63
+ this.cookie.path = path;
64
+ return this;
65
+ };
66
+
67
+ // Setter for cookie domain
68
+ this.setStorageCookieDomain = function(domain) {
69
+ this.cookie.domain = domain;
70
+ return this;
71
+ };
72
+
73
+ // Setter for notification config
74
+ // itemSet & itemRemove should be booleans
75
+ this.setNotify = function(itemSet, itemRemove) {
76
+ this.notify = {
77
+ setItem: itemSet,
78
+ removeItem: itemRemove
79
+ };
80
+ return this;
81
+ };
82
+
83
+ this.$get = ['$rootScope', '$window', '$document', '$parse', function($rootScope, $window, $document, $parse) {
84
+ var self = this;
85
+ var prefix = self.prefix;
86
+ var cookie = self.cookie;
87
+ var notify = self.notify;
88
+ var storageType = self.storageType;
89
+ var webStorage;
90
+
91
+ // When Angular's $document is not available
92
+ if (!$document) {
93
+ $document = document;
94
+ } else if ($document[0]) {
95
+ $document = $document[0];
96
+ }
97
+
98
+ // If there is a prefix set in the config lets use that with an appended period for readability
99
+ if (prefix.substr(-1) !== '.') {
100
+ prefix = !!prefix ? prefix + '.' : '';
101
+ }
102
+ var deriveQualifiedKey = function(key) {
103
+ return prefix + key;
104
+ };
105
+ // Checks the browser to see if local storage is supported
106
+ var browserSupportsLocalStorage = (function () {
107
+ try {
108
+ var supported = (storageType in $window && $window[storageType] !== null);
109
+
110
+ // When Safari (OS X or iOS) is in private browsing mode, it appears as though localStorage
111
+ // is available, but trying to call .setItem throws an exception.
112
+ //
113
+ // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to add something to storage
114
+ // that exceeded the quota."
115
+ var key = deriveQualifiedKey('__' + Math.round(Math.random() * 1e7));
116
+ if (supported) {
117
+ webStorage = $window[storageType];
118
+ webStorage.setItem(key, '');
119
+ webStorage.removeItem(key);
120
+ }
121
+
122
+ return supported;
123
+ } catch (e) {
124
+ storageType = 'cookie';
125
+ $rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
126
+ return false;
127
+ }
128
+ }());
129
+
130
+ // Directly adds a value to local storage
131
+ // If local storage is not available in the browser use cookies
132
+ // Example use: localStorageService.add('library','angular');
133
+ var addToLocalStorage = function (key, value) {
134
+ // Let's convert undefined values to null to get the value consistent
135
+ if (isUndefined(value)) {
136
+ value = null;
137
+ } else {
138
+ value = toJson(value);
139
+ }
140
+
141
+ // If this browser does not support local storage use cookies
142
+ if (!browserSupportsLocalStorage || self.storageType === 'cookie') {
143
+ if (!browserSupportsLocalStorage) {
144
+ $rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
145
+ }
146
+
147
+ if (notify.setItem) {
148
+ $rootScope.$broadcast('LocalStorageModule.notification.setitem', {key: key, newvalue: value, storageType: 'cookie'});
149
+ }
150
+ return addToCookies(key, value);
151
+ }
152
+
153
+ try {
154
+ if (webStorage) {
155
+ webStorage.setItem(deriveQualifiedKey(key), value);
156
+ }
157
+ if (notify.setItem) {
158
+ $rootScope.$broadcast('LocalStorageModule.notification.setitem', {key: key, newvalue: value, storageType: self.storageType});
159
+ }
160
+ } catch (e) {
161
+ $rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
162
+ return addToCookies(key, value);
163
+ }
164
+ return true;
165
+ };
166
+
167
+ // Directly get a value from local storage
168
+ // Example use: localStorageService.get('library'); // returns 'angular'
169
+ var getFromLocalStorage = function (key) {
170
+
171
+ if (!browserSupportsLocalStorage || self.storageType === 'cookie') {
172
+ if (!browserSupportsLocalStorage) {
173
+ $rootScope.$broadcast('LocalStorageModule.notification.warning','LOCAL_STORAGE_NOT_SUPPORTED');
174
+ }
175
+
176
+ return getFromCookies(key);
177
+ }
178
+
179
+ var item = webStorage ? webStorage.getItem(deriveQualifiedKey(key)) : null;
180
+ // angular.toJson will convert null to 'null', so a proper conversion is needed
181
+ // FIXME not a perfect solution, since a valid 'null' string can't be stored
182
+ if (!item || item === 'null') {
183
+ return null;
184
+ }
185
+
186
+ try {
187
+ return JSON.parse(item);
188
+ } catch (e) {
189
+ return item;
190
+ }
191
+ };
192
+
193
+ // Remove an item from local storage
194
+ // Example use: localStorageService.remove('library'); // removes the key/value pair of library='angular'
195
+ var removeFromLocalStorage = function () {
196
+ var i, key;
197
+ for (i=0; i<arguments.length; i++) {
198
+ key = arguments[i];
199
+ if (!browserSupportsLocalStorage || self.storageType === 'cookie') {
200
+ if (!browserSupportsLocalStorage) {
201
+ $rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
202
+ }
203
+
204
+ if (notify.removeItem) {
205
+ $rootScope.$broadcast('LocalStorageModule.notification.removeitem', {key: key, storageType: 'cookie'});
206
+ }
207
+ removeFromCookies(key);
208
+ }
209
+ else {
210
+ try {
211
+ webStorage.removeItem(deriveQualifiedKey(key));
212
+ if (notify.removeItem) {
213
+ $rootScope.$broadcast('LocalStorageModule.notification.removeitem', {
214
+ key: key,
215
+ storageType: self.storageType
216
+ });
217
+ }
218
+ } catch (e) {
219
+ $rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
220
+ removeFromCookies(key);
221
+ }
222
+ }
223
+ }
224
+ };
225
+
226
+ // Return array of keys for local storage
227
+ // Example use: var keys = localStorageService.keys()
228
+ var getKeysForLocalStorage = function () {
229
+
230
+ if (!browserSupportsLocalStorage) {
231
+ $rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
232
+ return false;
233
+ }
234
+
235
+ var prefixLength = prefix.length;
236
+ var keys = [];
237
+ for (var key in webStorage) {
238
+ // Only return keys that are for this app
239
+ if (key.substr(0,prefixLength) === prefix) {
240
+ try {
241
+ keys.push(key.substr(prefixLength));
242
+ } catch (e) {
243
+ $rootScope.$broadcast('LocalStorageModule.notification.error', e.Description);
244
+ return [];
245
+ }
246
+ }
247
+ }
248
+ return keys;
249
+ };
250
+
251
+ // Remove all data for this app from local storage
252
+ // Also optionally takes a regular expression string and removes the matching key-value pairs
253
+ // Example use: localStorageService.clearAll();
254
+ // Should be used mostly for development purposes
255
+ var clearAllFromLocalStorage = function (regularExpression) {
256
+
257
+ // Setting both regular expressions independently
258
+ // Empty strings result in catchall RegExp
259
+ var prefixRegex = !!prefix ? new RegExp('^' + prefix) : new RegExp();
260
+ var testRegex = !!regularExpression ? new RegExp(regularExpression) : new RegExp();
261
+
262
+ if (!browserSupportsLocalStorage || self.storageType === 'cookie') {
263
+ if (!browserSupportsLocalStorage) {
264
+ $rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
265
+ }
266
+ return clearAllFromCookies();
267
+ }
268
+
269
+ var prefixLength = prefix.length;
270
+
271
+ for (var key in webStorage) {
272
+ // Only remove items that are for this app and match the regular expression
273
+ if (prefixRegex.test(key) && testRegex.test(key.substr(prefixLength))) {
274
+ try {
275
+ removeFromLocalStorage(key.substr(prefixLength));
276
+ } catch (e) {
277
+ $rootScope.$broadcast('LocalStorageModule.notification.error',e.message);
278
+ return clearAllFromCookies();
279
+ }
280
+ }
281
+ }
282
+ return true;
283
+ };
284
+
285
+ // Checks the browser to see if cookies are supported
286
+ var browserSupportsCookies = (function() {
287
+ try {
288
+ return $window.navigator.cookieEnabled ||
289
+ ("cookie" in $document && ($document.cookie.length > 0 ||
290
+ ($document.cookie = "test").indexOf.call($document.cookie, "test") > -1));
291
+ } catch (e) {
292
+ $rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
293
+ return false;
294
+ }
295
+ }());
296
+
297
+ // Directly adds a value to cookies
298
+ // Typically used as a fallback is local storage is not available in the browser
299
+ // Example use: localStorageService.cookie.add('library','angular');
300
+ var addToCookies = function (key, value, daysToExpiry) {
301
+
302
+ if (isUndefined(value)) {
303
+ return false;
304
+ } else if(isArray(value) || isObject(value)) {
305
+ value = toJson(value);
306
+ }
307
+
308
+ if (!browserSupportsCookies) {
309
+ $rootScope.$broadcast('LocalStorageModule.notification.error', 'COOKIES_NOT_SUPPORTED');
310
+ return false;
311
+ }
312
+
313
+ try {
314
+ var expiry = '',
315
+ expiryDate = new Date(),
316
+ cookieDomain = '';
317
+
318
+ if (value === null) {
319
+ // Mark that the cookie has expired one day ago
320
+ expiryDate.setTime(expiryDate.getTime() + (-1 * 24 * 60 * 60 * 1000));
321
+ expiry = "; expires=" + expiryDate.toGMTString();
322
+ value = '';
323
+ } else if (isNumber(daysToExpiry) && daysToExpiry !== 0) {
324
+ expiryDate.setTime(expiryDate.getTime() + (daysToExpiry * 24 * 60 * 60 * 1000));
325
+ expiry = "; expires=" + expiryDate.toGMTString();
326
+ } else if (cookie.expiry !== 0) {
327
+ expiryDate.setTime(expiryDate.getTime() + (cookie.expiry * 24 * 60 * 60 * 1000));
328
+ expiry = "; expires=" + expiryDate.toGMTString();
329
+ }
330
+ if (!!key) {
331
+ var cookiePath = "; path=" + cookie.path;
332
+ if(cookie.domain){
333
+ cookieDomain = "; domain=" + cookie.domain;
334
+ }
335
+ $document.cookie = deriveQualifiedKey(key) + "=" + encodeURIComponent(value) + expiry + cookiePath + cookieDomain;
336
+ }
337
+ } catch (e) {
338
+ $rootScope.$broadcast('LocalStorageModule.notification.error',e.message);
339
+ return false;
340
+ }
341
+ return true;
342
+ };
343
+
344
+ // Directly get a value from a cookie
345
+ // Example use: localStorageService.cookie.get('library'); // returns 'angular'
346
+ var getFromCookies = function (key) {
347
+ if (!browserSupportsCookies) {
348
+ $rootScope.$broadcast('LocalStorageModule.notification.error', 'COOKIES_NOT_SUPPORTED');
349
+ return false;
350
+ }
351
+
352
+ var cookies = $document.cookie && $document.cookie.split(';') || [];
353
+ for(var i=0; i < cookies.length; i++) {
354
+ var thisCookie = cookies[i];
355
+ while (thisCookie.charAt(0) === ' ') {
356
+ thisCookie = thisCookie.substring(1,thisCookie.length);
357
+ }
358
+ if (thisCookie.indexOf(deriveQualifiedKey(key) + '=') === 0) {
359
+ var storedValues = decodeURIComponent(thisCookie.substring(prefix.length + key.length + 1, thisCookie.length));
360
+ try {
361
+ return JSON.parse(storedValues);
362
+ } catch(e) {
363
+ return storedValues;
364
+ }
365
+ }
366
+ }
367
+ return null;
368
+ };
369
+
370
+ var removeFromCookies = function (key) {
371
+ addToCookies(key,null);
372
+ };
373
+
374
+ var clearAllFromCookies = function () {
375
+ var thisCookie = null, thisKey = null;
376
+ var prefixLength = prefix.length;
377
+ var cookies = $document.cookie.split(';');
378
+ for(var i = 0; i < cookies.length; i++) {
379
+ thisCookie = cookies[i];
380
+
381
+ while (thisCookie.charAt(0) === ' ') {
382
+ thisCookie = thisCookie.substring(1, thisCookie.length);
383
+ }
384
+
385
+ var key = thisCookie.substring(prefixLength, thisCookie.indexOf('='));
386
+ removeFromCookies(key);
387
+ }
388
+ };
389
+
390
+ var getStorageType = function() {
391
+ return storageType;
392
+ };
393
+
394
+ // Add a listener on scope variable to save its changes to local storage
395
+ // Return a function which when called cancels binding
396
+ var bindToScope = function(scope, key, def, lsKey) {
397
+ lsKey = lsKey || key;
398
+ var value = getFromLocalStorage(lsKey);
399
+
400
+ if (value === null && isDefined(def)) {
401
+ value = def;
402
+ } else if (isObject(value) && isObject(def)) {
403
+ value = extend(def, value);
404
+ }
405
+
406
+ $parse(key).assign(scope, value);
407
+
408
+ return scope.$watch(key, function(newVal) {
409
+ addToLocalStorage(lsKey, newVal);
410
+ }, isObject(scope[key]));
411
+ };
412
+
413
+ // Return localStorageService.length
414
+ // ignore keys that not owned
415
+ var lengthOfLocalStorage = function() {
416
+ var count = 0;
417
+ var storage = $window[storageType];
418
+ for(var i = 0; i < storage.length; i++) {
419
+ if(storage.key(i).indexOf(prefix) === 0 ) {
420
+ count++;
421
+ }
422
+ }
423
+ return count;
424
+ };
425
+
426
+ return {
427
+ isSupported: browserSupportsLocalStorage,
428
+ getStorageType: getStorageType,
429
+ set: addToLocalStorage,
430
+ add: addToLocalStorage, //DEPRECATED
431
+ get: getFromLocalStorage,
432
+ keys: getKeysForLocalStorage,
433
+ remove: removeFromLocalStorage,
434
+ clearAll: clearAllFromLocalStorage,
435
+ bind: bindToScope,
436
+ deriveKey: deriveQualifiedKey,
437
+ length: lengthOfLocalStorage,
438
+ cookie: {
439
+ isSupported: browserSupportsCookies,
440
+ set: addToCookies,
441
+ add: addToCookies, //DEPRECATED
442
+ get: getFromCookies,
443
+ remove: removeFromCookies,
444
+ clearAll: clearAllFromCookies
445
+ }
446
+ };
447
+ }];
448
+ });
449
+ })( window, window.angular );