nanoc 2.0 → 2.1

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.
Files changed (99) hide show
  1. data/ChangeLog +54 -0
  2. data/LICENSE +1 -1
  3. data/README +63 -3
  4. data/Rakefile +61 -14
  5. data/bin/nanoc +7 -199
  6. data/lib/nanoc/base/asset.rb +113 -0
  7. data/lib/nanoc/base/asset_defaults.rb +21 -0
  8. data/lib/nanoc/base/asset_rep.rb +277 -0
  9. data/lib/nanoc/base/binary_filter.rb +44 -0
  10. data/lib/nanoc/base/code.rb +41 -0
  11. data/lib/nanoc/base/compiler.rb +46 -34
  12. data/lib/nanoc/base/core_ext/hash.rb +51 -7
  13. data/lib/nanoc/base/core_ext/string.rb +8 -0
  14. data/lib/nanoc/base/data_source.rb +253 -20
  15. data/lib/nanoc/base/defaults.rb +30 -0
  16. data/lib/nanoc/base/enhancements.rb +9 -84
  17. data/lib/nanoc/base/filter.rb +109 -6
  18. data/lib/nanoc/base/layout.rb +91 -0
  19. data/lib/nanoc/base/notification_center.rb +66 -0
  20. data/lib/nanoc/base/page.rb +94 -121
  21. data/lib/nanoc/base/page_defaults.rb +20 -0
  22. data/lib/nanoc/base/page_rep.rb +318 -0
  23. data/lib/nanoc/base/plugin.rb +57 -9
  24. data/lib/nanoc/base/proxies/asset_proxy.rb +29 -0
  25. data/lib/nanoc/base/proxies/asset_rep_proxy.rb +26 -0
  26. data/lib/nanoc/base/proxies/layout_proxy.rb +25 -0
  27. data/lib/nanoc/base/proxies/page_proxy.rb +35 -0
  28. data/lib/nanoc/base/proxies/page_rep_proxy.rb +28 -0
  29. data/lib/nanoc/base/proxy.rb +37 -0
  30. data/lib/nanoc/base/router.rb +72 -0
  31. data/lib/nanoc/base/site.rb +220 -91
  32. data/lib/nanoc/base/template.rb +64 -0
  33. data/lib/nanoc/binary_filters/image_science_thumbnail.rb +28 -0
  34. data/lib/nanoc/cli/base.rb +219 -0
  35. data/lib/nanoc/cli/cli.rb +16 -0
  36. data/lib/nanoc/cli/command.rb +105 -0
  37. data/lib/nanoc/cli/commands/autocompile.rb +80 -0
  38. data/lib/nanoc/cli/commands/compile.rb +273 -0
  39. data/lib/nanoc/cli/commands/create_layout.rb +85 -0
  40. data/lib/nanoc/cli/commands/create_page.rb +85 -0
  41. data/lib/nanoc/cli/commands/create_site.rb +327 -0
  42. data/lib/nanoc/cli/commands/create_template.rb +76 -0
  43. data/lib/nanoc/cli/commands/help.rb +69 -0
  44. data/lib/nanoc/cli/commands/info.rb +114 -0
  45. data/lib/nanoc/cli/commands/switch.rb +141 -0
  46. data/lib/nanoc/cli/commands/update.rb +91 -0
  47. data/lib/nanoc/cli/ext.rb +37 -0
  48. data/lib/nanoc/cli/logger.rb +66 -0
  49. data/lib/nanoc/cli/option_parser.rb +168 -0
  50. data/lib/nanoc/cli.rb +1 -0
  51. data/lib/nanoc/data_sources/filesystem.rb +650 -215
  52. data/lib/nanoc/data_sources/filesystem_combined.rb +495 -0
  53. data/lib/nanoc/extra/auto_compiler.rb +265 -0
  54. data/lib/nanoc/extra/context.rb +22 -0
  55. data/lib/nanoc/extra/core_ext/hash.rb +54 -0
  56. data/lib/nanoc/extra/core_ext/time.rb +13 -0
  57. data/lib/nanoc/extra/file_proxy.rb +29 -0
  58. data/lib/nanoc/extra/vcs.rb +48 -0
  59. data/lib/nanoc/extra/vcses/bazaar.rb +21 -0
  60. data/lib/nanoc/extra/vcses/dummy.rb +20 -0
  61. data/lib/nanoc/extra/vcses/git.rb +21 -0
  62. data/lib/nanoc/extra/vcses/mercurial.rb +21 -0
  63. data/lib/nanoc/extra/vcses/subversion.rb +21 -0
  64. data/lib/nanoc/filters/bluecloth.rb +13 -0
  65. data/lib/nanoc/filters/erb.rb +6 -22
  66. data/lib/nanoc/filters/erubis.rb +14 -0
  67. data/lib/nanoc/filters/haml.rb +11 -6
  68. data/lib/nanoc/filters/markaby.rb +5 -5
  69. data/lib/nanoc/filters/maruku.rb +14 -0
  70. data/lib/nanoc/filters/old.rb +19 -0
  71. data/lib/nanoc/filters/rdiscount.rb +13 -0
  72. data/lib/nanoc/filters/rdoc.rb +5 -4
  73. data/lib/nanoc/filters/redcloth.rb +14 -0
  74. data/lib/nanoc/filters/rubypants.rb +14 -0
  75. data/lib/nanoc/filters/sass.rb +13 -0
  76. data/lib/nanoc/helpers/blogging.rb +170 -0
  77. data/lib/nanoc/helpers/capturing.rb +59 -0
  78. data/lib/nanoc/helpers/html_escape.rb +23 -0
  79. data/lib/nanoc/helpers/link_to.rb +69 -0
  80. data/lib/nanoc/helpers/render.rb +47 -0
  81. data/lib/nanoc/helpers/tagging.rb +52 -0
  82. data/lib/nanoc/helpers/xml_sitemap.rb +58 -0
  83. data/lib/nanoc/routers/default.rb +54 -0
  84. data/lib/nanoc/routers/no_dirs.rb +66 -0
  85. data/lib/nanoc/routers/versioned.rb +79 -0
  86. data/lib/nanoc.rb +83 -12
  87. metadata +144 -47
  88. data/lib/nanoc/base/auto_compiler.rb +0 -124
  89. data/lib/nanoc/base/layout_processor.rb +0 -33
  90. data/lib/nanoc/base/page_proxy.rb +0 -31
  91. data/lib/nanoc/base/plugin_manager.rb +0 -33
  92. data/lib/nanoc/data_sources/database.rb +0 -259
  93. data/lib/nanoc/data_sources/trivial.rb +0 -145
  94. data/lib/nanoc/filters/markdown.rb +0 -13
  95. data/lib/nanoc/filters/smartypants.rb +0 -13
  96. data/lib/nanoc/filters/textile.rb +0 -13
  97. data/lib/nanoc/layout_processors/erb.rb +0 -35
  98. data/lib/nanoc/layout_processors/haml.rb +0 -18
  99. data/lib/nanoc/layout_processors/markaby.rb +0 -16
@@ -0,0 +1,141 @@
1
+ module Nanoc::CLI
2
+
3
+ class SwitchCommand < Command # :nodoc:
4
+
5
+ def name
6
+ 'switch'
7
+ end
8
+
9
+ def aliases
10
+ []
11
+ end
12
+
13
+ def short_desc
14
+ 'switch the site to a new data source'
15
+ end
16
+
17
+ def long_desc
18
+ 'Move the data stored in the site to a new, given data source.' +
19
+ "\n" +
20
+ 'The given new data source may need additional configuration ' +
21
+ 'parameters that cannot be specified on the commandline. These ' +
22
+ 'should be stored in the configuration file (config.yaml) BEFORE ' +
23
+ 'executing the switch command.' +
24
+ "\n" +
25
+ 'This command first loads all existing data into memory, destroys ' +
26
+ 'the on-disk data, changes the site\'s data source, and finally ' +
27
+ 'writes the data back to the disk using the new data source. Because ' +
28
+ 'of this action\'s destructive nature, THIS OPERATION SHOULD NOT BE ' +
29
+ 'INTERRUPTED as interruption could result in data loss.' +
30
+ "\n" +
31
+ 'This command will change data, and it is therefore recommended to ' +
32
+ 'make a backup in case something goes wrong.'
33
+ end
34
+
35
+ def usage
36
+ "nanoc switch [options]"
37
+ end
38
+
39
+ def option_definitions
40
+ [
41
+ # --vcs
42
+ {
43
+ :long => 'vcs', :short => 'c', :argument => :required,
44
+ :desc => 'select the VCS to use'
45
+ },
46
+ # --yes
47
+ {
48
+ :long => 'yes', :short => 'y', :argument => :forbidden,
49
+ :desc => 'switches the data source without warning'
50
+ },
51
+ # --datasource
52
+ {
53
+ :long => 'datasource', :short => 'd', :argument => :required,
54
+ :desc => 'specify the new data source for the site'
55
+ }
56
+ ]
57
+ end
58
+
59
+ def run(options, arguments)
60
+ # Check arguments
61
+ if arguments.size != 0
62
+ $stderr.puts "usage: #{usage}"
63
+ exit 1
64
+ end
65
+
66
+ # Check options
67
+ unless options.has_key?(:datasource)
68
+ $stderr.puts 'A new data source should be specified using the ' +
69
+ '-d/--datasource option.'
70
+ exit 1
71
+ end
72
+
73
+ # Find data source
74
+ data_source = Nanoc::DataSource.named(options[:datasource])
75
+ if data_source.nil?
76
+ $stderr.puts "Unrecognised data source: #{options[:datasource]}"
77
+ exit 1
78
+ end
79
+
80
+ # Make sure we are in a nanoc site directory
81
+ @base.require_site
82
+
83
+ # Set VCS if possible
84
+ @base.set_vcs(options[:vcs])
85
+
86
+ # Check for -y switch
87
+ unless options.has_key?(:yes)
88
+ $stderr.puts '*************'
89
+ $stderr.puts '** WARNING **'
90
+ $stderr.puts '*************'
91
+ $stderr.puts
92
+ $stderr.puts 'Are you absolutely sure you want to set up the data ' +
93
+ 'source for this site? Setting up the data source ' +
94
+ 'will remove existing data. This operation is ' +
95
+ 'destructive and cannot be reverted. Please do not ' +
96
+ 'interrupt this operation; doing so can result in ' +
97
+ 'data loss. As always, consider making a backup copy.'
98
+ $stderr.puts
99
+ $stderr.puts 'To continue, use the -y/--yes option, like "nanoc ' +
100
+ 'switch -y".'
101
+ exit 1
102
+ end
103
+
104
+ # Setup notifications
105
+ Nanoc::NotificationCenter.on(:file_created) do |file_path|
106
+ Nanoc::CLI::Logger.instance.file(:high, :create, file_path)
107
+ end
108
+ Nanoc::NotificationCenter.on(:file_updated) do |file_path|
109
+ Nanoc::CLI::Logger.instance.file(:high, :update, file_path)
110
+ end
111
+
112
+ # Load data
113
+ @base.site.load_data
114
+
115
+ # Destroy existing data
116
+ @base.site.data_source.destroy
117
+
118
+ # Update configuration
119
+ @base.site.config[:data_source] = options[:datasource]
120
+ @base.site.instance_eval { @data_source = data_source.new(self) }
121
+ File.open('config.yaml', 'w') { |io| io.write(YAML.dump(@base.site.config.stringify_keys)) }
122
+
123
+ # Set VCS on new data source if possible
124
+ @base.set_vcs(options[:vcs])
125
+
126
+ @base.site.data_source.loading do
127
+ # Create initial data source
128
+ @base.site.data_source.setup
129
+
130
+ # Store all data
131
+ @base.site.pages.each { |p| @base.site.data_source.save_page(p) }
132
+ @base.site.data_source.save_page_defaults(@base.site.page_defaults)
133
+ @base.site.layouts.each { |l| @base.site.data_source.save_layout(l) }
134
+ @base.site.templates.each { |t| @base.site.data_source.save_template(t) }
135
+ @base.site.data_source.save_code(@base.site.code)
136
+ end
137
+ end
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,91 @@
1
+ module Nanoc::CLI
2
+
3
+ class UpdateCommand < Command # :nodoc:
4
+
5
+ def name
6
+ 'update'
7
+ end
8
+
9
+ def aliases
10
+ []
11
+ end
12
+
13
+ def short_desc
14
+ 'update the data stored by the data source to a newer version'
15
+ end
16
+
17
+ def long_desc
18
+ 'Update the data stored by the data source to a newer format. The ' +
19
+ 'format in which data is stored can change between releases, and ' +
20
+ 'even though backward compatibility is usually preserved, it is ' +
21
+ 'often a good idea to store the site data in a newer format so newer ' +
22
+ 'features can be taken advantage of.' +
23
+ "\n" +
24
+ 'This command will change data, and it is therefore recommended to ' +
25
+ 'make a backup in case something goes wrong.'
26
+ end
27
+
28
+ def usage
29
+ "nanoc update [options]"
30
+ end
31
+
32
+ def option_definitions
33
+ [
34
+ # --vcs
35
+ {
36
+ :long => 'vcs', :short => 'c', :argument => :required,
37
+ :desc => 'select the VCS to use'
38
+ },
39
+ # --yes
40
+ {
41
+ :long => 'yes', :short => 'y', :argument => :forbidden,
42
+ :desc => 'updates the data without warning'
43
+ }
44
+ ]
45
+ end
46
+
47
+ def run(options, arguments)
48
+ # Check arguments
49
+ if arguments.size != 0
50
+ $stderr.puts "usage: #{usage}"
51
+ exit 1
52
+ end
53
+
54
+ # Make sure we are in a nanoc site directory
55
+ @base.require_site
56
+
57
+ # Set VCS if possible
58
+ @base.set_vcs(options[:vcs])
59
+
60
+ # Check for -y switch
61
+ unless options.has_key?(:yes)
62
+ $stderr.puts '*************'
63
+ $stderr.puts '** WARNING **'
64
+ $stderr.puts '*************'
65
+ $stderr.puts
66
+ $stderr.puts 'Are you absolutely sure you want to update the ' +
67
+ 'content for this site? Updating the site content ' +
68
+ 'will change the structure of existing data. This ' +
69
+ 'operation is destructive and cannot be reverted. ' +
70
+ 'Please do not interrupt this operation; doing so can ' +
71
+ 'result in data loss. As always, consider making a ' +
72
+ 'backup copy.'
73
+ $stderr.puts
74
+ $stderr.puts 'If this nanoc site is versioned using a VCS ' +
75
+ 'supported by nanoc, consider using the --vcs option ' +
76
+ 'to have nanoc perform add/delete/move operations ' +
77
+ 'using the specified VCS. To get a list of VCSes ' +
78
+ 'supported by nanoc, issue the "info" command.'
79
+ $stderr.puts
80
+ $stderr.puts 'To continue, use the -y/--yes option, like "nanoc ' +
81
+ 'update -y".'
82
+ exit 1
83
+ end
84
+
85
+ # Update
86
+ @base.site.data_source.update
87
+ end
88
+
89
+ end
90
+
91
+ end
@@ -0,0 +1,37 @@
1
+ class String
2
+
3
+ # Word-wraps and indents the string.
4
+ #
5
+ # +width+:: The maximal width of each line. This also includes indentation,
6
+ # i.e. the actual maximal width of the text is width-indentation.
7
+ #
8
+ # +indentation+:: The number of spaces to indent each wrapped line.
9
+ def wrap_and_indent(width, indentation)
10
+ # Split into paragraphs
11
+ paragraphs = self.split("\n").map { |p| p.strip }.reject { |p| p == '' }
12
+
13
+ # Wrap and indent each paragraph
14
+ paragraphs.map do |paragraph|
15
+ # Initialize
16
+ lines = []
17
+ line = ''
18
+
19
+ # Split into words
20
+ paragraph.split(/\s/).each do |word|
21
+ # Begin new line if it's too long
22
+ if (line + ' ' + word).length >= width
23
+ lines << line
24
+ line = ''
25
+ end
26
+
27
+ # Add word to line
28
+ line += (line == '' ? '' : ' ' ) + word
29
+ end
30
+ lines << line
31
+
32
+ # Join lines
33
+ lines.map { |l| ' '*indentation + l }.join("\n")
34
+ end.join("\n\n")
35
+ end
36
+
37
+ end
@@ -0,0 +1,66 @@
1
+ require 'singleton'
2
+
3
+ module Nanoc::CLI
4
+
5
+ # Nanoc::CLI::Logger is a singleton class responsible for generating
6
+ # feedback in the terminal.
7
+ class Logger
8
+
9
+ ACTION_COLORS = {
10
+ :create => "\e[1m" + "\e[32m", # bold + green
11
+ :update => "\e[1m" + "\e[33m", # bold + yellow
12
+ :identical => "\e[1m", # bold
13
+ :skip => "\e[1m" # bold
14
+ }
15
+
16
+ include Singleton
17
+
18
+ # The log leve, which can be :high, :low or :off (which will log all
19
+ # messages, only high-priority messages, or no messages at all,
20
+ # respectively).
21
+ attr_accessor :level
22
+
23
+ def initialize # :nodoc:
24
+ @level = :high
25
+ end
26
+
27
+ # Logs a file-related action.
28
+ #
29
+ # +level+:: The importance of this action. Can be :high or :low.
30
+ #
31
+ # +action+:: The kind of file action. Can be :create, :update or
32
+ # :identical.
33
+ #
34
+ # +path+:: The path to the file the action was performed on.
35
+ def file(level, action, path, duration=nil)
36
+ log(
37
+ level,
38
+ '%s%12s%s %s%s' % [
39
+ ACTION_COLORS[action.to_sym],
40
+ action,
41
+ "\e[0m",
42
+ duration.nil? ? '' : "[%2.2fs] " % [ duration ],
43
+ path
44
+ ]
45
+ )
46
+ end
47
+
48
+ # Logs a message.
49
+ #
50
+ # +level+:: The importance of this message. Can be :high or :low.
51
+ #
52
+ # +s+:: The message to be logged.
53
+ #
54
+ # +io+:: The IO instance to which the message will be written. Defaults to
55
+ # standard output.
56
+ def log(level, s, io=$stdout)
57
+ # Don't log when logging is disabled
58
+ return if @level == :off
59
+
60
+ # Log when level permits it
61
+ io.puts(s) if (@level == :low or @level == level)
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,168 @@
1
+ module Nanoc::CLI
2
+
3
+ # Nanoc::CLI::OptionParser is used for parsing commandline options.
4
+ class OptionParser
5
+
6
+ # Error that will be raised when an unknown option is encountered.
7
+ class IllegalOptionError < RuntimeError ; end
8
+
9
+ # Error that will be raised when an option without argument is
10
+ # encountered.
11
+ class OptionRequiresAnArgumentError < RuntimeError ; end
12
+
13
+ # Parses the commandline arguments in +arguments_and_options+, using the
14
+ # commandline option definitions in +definitions+.
15
+ #
16
+ # +arguments_and_options+ is an array of commandline arguments and
17
+ # options. This will usually be +ARGV+.
18
+ #
19
+ # +definitions+ contains a list of hashes defining which options are
20
+ # allowed and how they will be handled. Such a hash has three keys:
21
+ #
22
+ # :short:: The short name of the option, e.g. +a+. Do not include the '-'
23
+ # prefix.
24
+ #
25
+ # :long:: The long name of the option, e.g. +all+. Do not include the '--'
26
+ # prefix.
27
+ #
28
+ # :argument:: Whether this option's argument is required (:required) or
29
+ # forbidden (:forbidden).
30
+ #
31
+ # A sample array of definition hashes could look like this:
32
+ #
33
+ # [
34
+ # { :short => 'a', :long => 'all', :argument => :forbidden },
35
+ # { :short => 'p', :long => 'port', :argument => :required },
36
+ # ]
37
+ #
38
+ # During parsing, two errors can be raised:
39
+ #
40
+ # IllegalOptionError:: An unrecognised option was encountered, i.e. an
41
+ # option that is not present in the list of option
42
+ # definitions.
43
+ #
44
+ # OptionRequiresAnArgumentError:: An option was found that did not have a
45
+ # value, even though this value was
46
+ # required.
47
+ #
48
+ # What will be returned, is a hash with two keys, :arguments and :options.
49
+ # The :arguments value contains a list of arguments, and the :options
50
+ # value contains a hash with key-value pairs for each option. Options
51
+ # without values will have a +nil+ value instead.
52
+ #
53
+ # For example, the following commandline options (which should not be
54
+ # passed as a string, but as an array of strings):
55
+ #
56
+ # foo bar -xyz -a hiss --level 50 --father=ani -n luke squeak
57
+ #
58
+ # with the following option definitions:
59
+ #
60
+ # [
61
+ # { :short => 'x', :long => 'xxx', :argument => :forbidden },
62
+ # { :short => 'y', :long => 'yyy', :argument => :forbidden },
63
+ # { :short => 'z', :long => 'zzz', :argument => :forbidden },
64
+ # { :short => 'a', :long => 'all', :argument => :forbidden },
65
+ # { :short => 'l', :long => 'level', :argument => :required },
66
+ # { :short => 'f', :long => 'father', :argument => :required },
67
+ # { :short => 'n', :long => 'name', :argument => :required }
68
+ # ]
69
+ #
70
+ # will be translated into:
71
+ #
72
+ # {
73
+ # :arguments => [ 'foo', 'bar', 'hiss', 'squeak' ],
74
+ # :options => {
75
+ # :xxx => nil,
76
+ # :yyy => nil,
77
+ # :zzz => nil,
78
+ # :all => nil,
79
+ # :level => '50',
80
+ # :father => 'ani',
81
+ # :name => 'luke'
82
+ # }
83
+ # }
84
+ def self.parse(arguments_and_options, definitions)
85
+ # Don't touch original argument
86
+ unprocessed_arguments_and_options = arguments_and_options.dup
87
+
88
+ # Initialize
89
+ arguments = []
90
+ options = {}
91
+
92
+ # Determines whether we've passed the '--' marker or not
93
+ no_more_options = false
94
+
95
+ loop do
96
+ # Get next item
97
+ e = unprocessed_arguments_and_options.shift
98
+ break if e.nil?
99
+
100
+ # Handle end-of-options marker
101
+ if e == '--'
102
+ no_more_options = true
103
+ # Handle incomplete options
104
+ elsif e =~ /^--./ and !no_more_options
105
+ # Get option key, and option value if included
106
+ if e =~ /^--([^=]+)=(.+)$/
107
+ option_key = $1
108
+ option_value = $2
109
+ else
110
+ option_key = e[2..-1]
111
+ option_value = nil
112
+ end
113
+
114
+ # Find definition
115
+ definition = definitions.find { |d| d[:long] == option_key }
116
+ raise IllegalOptionError.new(option_key) if definition.nil?
117
+
118
+ if definition[:argument] == :required
119
+ # Get option value if necessary
120
+ if option_value.nil?
121
+ option_value = unprocessed_arguments_and_options.shift
122
+ raise OptionRequiresAnArgumentError.new(option_key) if option_value.nil?
123
+ end
124
+
125
+ # Store option
126
+ options[definition[:long].to_sym] = option_value
127
+ else
128
+ # Store option
129
+ options[definition[:long].to_sym] = nil
130
+ end
131
+ # Handle -xyz options
132
+ elsif e =~ /^-./ and !no_more_options
133
+ # Get option keys
134
+ option_keys = e[1..-1].scan(/./)
135
+
136
+ # For each key
137
+ option_keys.each do |option_key|
138
+ # Find definition
139
+ definition = definitions.find { |d| d[:short] == option_key }
140
+ raise IllegalOptionError.new(option_key) if definition.nil?
141
+
142
+ if option_keys.length > 1 and definition[:argument] == :required
143
+ # This is a combined option and it requires an argument, so complain
144
+ raise OptionRequiresAnArgumentError.new(option_key) if option_value.nil?
145
+ elsif definition[:argument] == :required
146
+ # Get option value
147
+ option_value = unprocessed_arguments_and_options.shift
148
+ raise OptionRequiresAnArgumentError.new(option_key) if option_value.nil?
149
+
150
+ # Store option
151
+ options[definition[:long].to_sym] = option_value
152
+ else
153
+ # Store option
154
+ options[definition[:long].to_sym] = nil
155
+ end
156
+ end
157
+ # Handle normal arguments
158
+ else
159
+ arguments << e
160
+ end
161
+ end
162
+
163
+ { :options => options, :arguments => arguments }
164
+ end
165
+
166
+ end
167
+
168
+ end
data/lib/nanoc/cli.rb ADDED
@@ -0,0 +1 @@
1
+ Nanoc.load('cli', '*.rb')