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