songbooks 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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/Guardfile +29 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/application.coffee +4 -0
- data/app/assets/javascripts/components/filters/convertWhitespace.filter.coffee +3 -0
- data/app/assets/javascripts/components/song_component/chord.directive.coffee +9 -0
- data/app/assets/javascripts/components/song_component/literal.directive.coffee +6 -0
- data/app/assets/javascripts/components/song_component/song_component.directive.coffee +6 -0
- data/app/assets/javascripts/components/songbook/songbook.controller.coffee +29 -0
- data/app/assets/javascripts/components/songs/song.controller.coffee +10 -0
- data/app/assets/javascripts/components/songs/song.directive.coffee +7 -0
- data/app/assets/javascripts/components/songs/song.service.coffee +98 -0
- data/app/assets/javascripts/components/songs/songs.controller.coffee +21 -0
- data/app/assets/javascripts/routes.coffee +11 -0
- data/app/assets/stylesheets/application.scss +34 -0
- data/app/fonts/DejaVuSans-Bold.ttf +0 -0
- data/app/fonts/DejaVuSans-BoldOblique.ttf +0 -0
- data/app/fonts/DejaVuSans-ExtraLight.ttf +0 -0
- data/app/fonts/DejaVuSans-Oblique.ttf +0 -0
- data/app/fonts/DejaVuSans.ttf +0 -0
- data/app/fonts/DejaVuSansCondensed-Bold.ttf +0 -0
- data/app/fonts/DejaVuSansCondensed-BoldOblique.ttf +0 -0
- data/app/fonts/DejaVuSansCondensed-Oblique.ttf +0 -0
- data/app/fonts/DejaVuSansCondensed.ttf +0 -0
- data/app/fonts/DejaVuSansMono-Bold.ttf +0 -0
- data/app/fonts/DejaVuSansMono-BoldOblique.ttf +0 -0
- data/app/fonts/DejaVuSansMono-Oblique.ttf +0 -0
- data/app/fonts/DejaVuSansMono.ttf +0 -0
- data/app/fonts/DejaVuSerif-Bold.ttf +0 -0
- data/app/fonts/DejaVuSerif-BoldItalic.ttf +0 -0
- data/app/fonts/DejaVuSerif-Italic.ttf +0 -0
- data/app/fonts/DejaVuSerif.ttf +0 -0
- data/app/fonts/DejaVuSerifCondensed-Bold.ttf +0 -0
- data/app/fonts/DejaVuSerifCondensed-BoldItalic.ttf +0 -0
- data/app/fonts/DejaVuSerifCondensed-Italic.ttf +0 -0
- data/app/fonts/DejaVuSerifCondensed.ttf +0 -0
- data/app/tex/songbook.tex +12 -0
- data/app/views/application.haml +54 -0
- data/app/views/layouts/application.haml +40 -0
- data/app/views/songbook.prawn +80 -0
- data/app/views/songs/component.haml +2 -0
- data/app/views/songs/index.jbuilder +5 -0
- data/app/views/songs/show.haml +23 -0
- data/app/views/songs/show.jbuilder +24 -0
- data/app/views/songs/song.haml +4 -0
- data/exe/sb_server +6 -0
- data/lib/prawn_ext/font/afm.rb +20 -0
- data/lib/songbooks.rb +32 -0
- data/lib/songbooks/CLI.rb +13 -0
- data/lib/songbooks/components/chord.rb +98 -0
- data/lib/songbooks/components/literal.rb +57 -0
- data/lib/songbooks/components/metadata.rb +69 -0
- data/lib/songbooks/components/section.rb +73 -0
- data/lib/songbooks/controllers/controller.rb +18 -0
- data/lib/songbooks/controllers/songs_controller.rb +30 -0
- data/lib/songbooks/folder.rb +32 -0
- data/lib/songbooks/server.rb +65 -0
- data/lib/songbooks/song.rb +53 -0
- data/lib/songbooks/version.rb +3 -0
- data/lib/songbooks/view_helper.rb +9 -0
- data/public/javascripts/angular-local-storage.js +449 -0
- data/public/javascripts/angular-route.js +991 -0
- data/public/javascripts/angular.js +28904 -0
- data/public/javascripts/application.js +8 -0
- data/public/javascripts/components/filters/convertWhitespace.filter.js +8 -0
- data/public/javascripts/components/song_component/chord.directive.js +13 -0
- data/public/javascripts/components/song_component/literal.directive.js +9 -0
- data/public/javascripts/components/song_component/song_component.directive.js +9 -0
- data/public/javascripts/components/songbook/songbook.controller.js +33 -0
- data/public/javascripts/components/songs/song.controller.js +12 -0
- data/public/javascripts/components/songs/song.directive.js +9 -0
- data/public/javascripts/components/songs/song.service.js +107 -0
- data/public/javascripts/components/songs/songs.controller.js +22 -0
- data/public/javascripts/jquery-2.1.4.min.js +4 -0
- data/public/javascripts/routes.js +16 -0
- data/public/stylesheets/application.css +24 -0
- data/public/stylesheets/bootstrap-theme.min.css +5 -0
- data/public/stylesheets/bootstrap.min.css +5 -0
- data/songbooks.gemspec +51 -0
- 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,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 );
|