1 /*=============================================================================
2 Copyright (c) 2002 2004 2006 Joel de Guzman
3 Copyright (c) 2004 Eric Niebler
4 Copyright (c) 2005 Thomas Guest
5 Copyright (c) 2013 Daniel James
7 Use, modification and distribution is subject to the Boost Software
8 License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
9 http://www.boost.org/LICENSE_1_0.txt)
10 =============================================================================*/
12 #include "native_text.hpp"
14 #include "include_paths.hpp"
17 #include "quickbook.hpp" // For the include_path global (yuck)
18 #include <boost/foreach.hpp>
19 #include <boost/range/algorithm/replace.hpp>
20 #include <boost/filesystem/operations.hpp>
29 path_parameter check_path(value const& path, quickbook::state& state)
31 if (qbk_version_n >= 107u) {
32 std::string path_text = path.get_encoded();
33 if (path_text.empty())
35 detail::outerr(path.get_file(), path.get_position())
36 << "Empty path argument"
39 return path_parameter(path_text, path_parameter::invalid);
43 if (check_glob(path_text)) {
44 return path_parameter(path_text, path_parameter::glob);
47 return path_parameter(glob_unescape(path_text),
48 path_parameter::path);
50 } catch(glob_error& e) {
51 detail::outerr(path.get_file(), path.get_position())
52 << "Invalid path (" << e.what() << "): "
56 return path_parameter(path_text, path_parameter::invalid);
60 // Paths are encoded for quickbook 1.6+ and also xmlbase
61 // values (technically xmlbase is a 1.6 feature, but that
62 // isn't enforced as it's backwards compatible).
64 // Counter-intuitively: encoded == plain text here.
66 std::string path_text = qbk_version_n >= 106u || path.is_encoded() ?
67 path.get_encoded() : detail::to_s(path.get_quickbook());
69 if (path_text.empty())
71 detail::outerr(path.get_file(), path.get_position())
72 << "Empty path argument"
75 return path_parameter(path_text, path_parameter::invalid);
78 // Check for windows paths, an error in quickbook 1.6
79 // In quickbook 1.7 backslash is used as an escape character
80 // for glob characters.
81 if (path_text.find('\\') != std::string::npos)
83 quickbook::detail::ostream* err;
85 if (qbk_version_n >= 106u) {
86 err = &detail::outerr(path.get_file(), path.get_position());
90 err = &detail::outwarn(path.get_file(), path.get_position());
93 *err << "Path isn't portable: '"
98 boost::replace(path_text, '\\', '/');
101 return path_parameter(path_text, path_parameter::path);
105 path_parameter check_xinclude_path(value const& p, quickbook::state& state)
107 path_parameter parameter = check_path(p, state);
109 if (parameter.type == path_parameter::glob) {
110 detail::outerr(p.get_file(), p.get_position())
111 << "Glob used for xml path."
114 parameter.type = path_parameter::invalid;
121 // Search include path
124 void include_search_glob(std::set<quickbook_path> & result,
125 quickbook_path const& location,
126 std::string path, quickbook::state& state)
128 std::size_t glob_pos = find_glob_char(path);
130 if (glob_pos == std::string::npos)
132 quickbook_path complete_path = location / glob_unescape(path);
134 if (fs::exists(complete_path.file_path))
136 state.dependencies.add_glob_match(complete_path.file_path);
137 result.insert(complete_path);
142 std::size_t prev = path.rfind('/', glob_pos);
143 std::size_t next = path.find('/', glob_pos);
145 std::size_t glob_begin = prev == std::string::npos ? 0 : prev + 1;
146 std::size_t glob_end = next == std::string::npos ? path.size() : next;
148 quickbook_path new_location = location;
150 if (prev != std::string::npos) {
151 new_location /= glob_unescape(path.substr(0, prev));
154 if (next != std::string::npos) ++next;
156 quickbook::string_view glob(
157 path.data() + glob_begin,
158 glob_end - glob_begin);
160 fs::path base_dir = new_location.file_path.empty() ?
161 fs::path(".") : new_location.file_path;
162 if (!fs::is_directory(base_dir)) return;
164 // Walk through the dir for matches.
165 for (fs::directory_iterator dir_i(base_dir), dir_e;
166 dir_i != dir_e; ++dir_i)
168 fs::path f = dir_i->path().filename();
169 std::string generic_path = detail::path_to_generic(f);
171 // Skip if the dir item doesn't match.
172 if (!quickbook::glob(glob, generic_path)) continue;
174 // If it's a file we add it to the results.
175 if (next == std::string::npos)
177 if (fs::is_regular_file(dir_i->status()))
179 quickbook_path r = new_location / generic_path;
180 state.dependencies.add_glob_match(r.file_path);
184 // If it's a matching dir, we recurse looking for more files.
187 if (!fs::is_regular_file(dir_i->status()))
189 include_search_glob(result, new_location / generic_path,
190 path.substr(next), state);
196 std::set<quickbook_path> include_search(path_parameter const& parameter,
197 quickbook::state& state, string_iterator pos)
199 std::set<quickbook_path> result;
201 switch (parameter.type) {
202 case path_parameter::glob:
203 // If the path has some glob match characters
204 // we do a discovery of all the matches..
206 fs::path current = state.current_file->path.parent_path();
208 // Search for the current dir accumulating to the result.
209 state.dependencies.add_glob(current / parameter.value);
210 include_search_glob(result, state.current_path.parent_path(),
211 parameter.value, state);
213 // Search the include path dirs accumulating to the result.
215 BOOST_FOREACH(fs::path dir, include_path)
218 state.dependencies.add_glob(dir / parameter.value);
219 include_search_glob(result,
220 quickbook_path(dir, count, fs::path()),
221 parameter.value, state);
228 case path_parameter::path:
230 fs::path path = detail::generic_to_path(parameter.value);
232 // If the path is relative, try and resolve it.
233 if (!path.has_root_directory() && !path.has_root_name())
235 quickbook_path path2 =
236 state.current_path.parent_path() / parameter.value;
238 // See if it can be found locally first.
239 if (state.dependencies.add_dependency(path2.file_path))
241 result.insert(path2);
245 // Search in each of the include path locations.
247 BOOST_FOREACH(fs::path full, include_path)
252 if (state.dependencies.add_dependency(full))
254 result.insert(quickbook_path(full, count, path));
261 if (state.dependencies.add_dependency(path)) {
262 result.insert(quickbook_path(path, 0, path));
267 detail::outerr(state.current_file, pos)
268 << "Unable to find file: "
276 case path_parameter::invalid:
289 void swap(quickbook_path& x, quickbook_path& y) {
290 boost::swap(x.file_path, y.file_path);
291 boost::swap(x.include_path_offset, y.include_path_offset);
292 boost::swap(x.abstract_file_path, y.abstract_file_path);
295 bool quickbook_path::operator<(quickbook_path const& other) const
297 // TODO: Is comparing file_path redundant? Surely if quickbook_path
298 // and abstract_file_path are equal, it must also be.
299 // (but not vice-versa)
301 abstract_file_path != other.abstract_file_path ?
302 abstract_file_path < other.abstract_file_path :
303 include_path_offset != other.include_path_offset ?
304 include_path_offset < other.include_path_offset :
305 file_path < other.file_path;
308 quickbook_path quickbook_path::operator/(quickbook::string_view x) const
310 return quickbook_path(*this) /= x;
313 quickbook_path& quickbook_path::operator/=(quickbook::string_view x)
315 fs::path x2 = detail::generic_to_path(x);
317 abstract_file_path /= x2;
321 quickbook_path quickbook_path::parent_path() const
323 return quickbook_path(file_path.parent_path(), include_path_offset,
324 abstract_file_path.parent_path());
327 // Not a general purpose normalization function, just
328 // from paths from the root directory. It strips the excess
329 // ".." parts from a path like: "x/../../y", leaving "y".
330 std::vector<fs::path> normalize_path_from_root(fs::path const& path)
332 assert(!path.has_root_directory() && !path.has_root_name());
334 std::vector<fs::path> parts;
336 BOOST_FOREACH(fs::path const& part, path)
338 if (part.empty() || part == ".") {
340 else if (part == "..") {
341 if (!parts.empty()) parts.pop_back();
344 parts.push_back(part);
351 // The relative path from base to path
352 fs::path path_difference(fs::path const& base, fs::path const& path)
355 absolute_base = fs::absolute(base),
356 absolute_path = fs::absolute(path);
358 // Remove '.', '..' and empty parts from the remaining path
359 std::vector<fs::path>
360 base_parts = normalize_path_from_root(absolute_base.relative_path()),
361 path_parts = normalize_path_from_root(absolute_path.relative_path());
363 std::vector<fs::path>::iterator
364 base_it = base_parts.begin(),
365 base_end = base_parts.end(),
366 path_it = path_parts.begin(),
367 path_end = path_parts.end();
369 // Build up the two paths in these variables, checking for the first
372 base_tmp = absolute_base.root_path(),
373 path_tmp = absolute_path.root_path();
377 // If they have different roots then there's no relative path so
378 // just build an absolute path.
379 if (!fs::equivalent(base_tmp, path_tmp))
385 // Find the point at which the paths differ
386 for(; base_it != base_end && path_it != path_end; ++base_it, ++path_it)
388 if(!fs::equivalent(base_tmp /= *base_it, path_tmp /= *path_it))
392 // Build a relative path to that point
393 for(; base_it != base_end; ++base_it) result /= "..";
396 // Build the rest of our path
397 for(; path_it != path_end; ++path_it) result /= *path_it;
402 // Convert a Boost.Filesystem path to a URL.
404 // I'm really not sure about this, as the meaning of root_name and
405 // root_directory are only clear for windows.
407 // Some info on file URLs at:
408 // https://en.wikipedia.org/wiki/File_URI_scheme
409 std::string file_path_to_url(fs::path const& x)
411 // TODO: Maybe some kind of error if this doesn't understand the path.
412 // TODO: Might need a special cygwin implementation.
413 // TODO: What if x.has_root_name() && !x.has_root_directory()?
414 // TODO: What does Boost.Filesystem do for '//localhost/c:/path'?
415 // Is that event allowed by windows?
417 if (x.has_root_name()) {
418 std::string root_name = detail::path_to_generic(x.root_name());
420 if (root_name.size() > 2 && root_name[0] == '/' && root_name[1] == '/') {
421 // root_name is a network location.
422 return "file:" + detail::escape_uri(detail::path_to_generic(x));
424 else if (root_name.size() >= 2 && root_name[root_name.size() - 1] == ':') {
425 // root_name is a drive.
427 + detail::escape_uri(root_name.substr(0, root_name.size() - 1))
428 + ":/" // TODO: Or maybe "|/".
429 + detail::escape_uri(detail::path_to_generic(x.relative_path()));
432 // Not sure what root_name is.
433 return detail::escape_uri(detail::path_to_generic(x));
436 else if (x.has_root_directory()) {
437 return "file://" + detail::escape_uri(detail::path_to_generic(x));
440 return detail::escape_uri(detail::path_to_generic(x));
444 std::string dir_path_to_url(fs::path const& x)
446 return file_path_to_url(x) + "/";
449 quickbook_path resolve_xinclude_path(std::string const& x, quickbook::state& state) {
450 fs::path path = detail::generic_to_path(x);
451 fs::path full_path = path;
453 // If the path is relative
454 if (!path.has_root_directory())
456 // Resolve the path from the current file
457 full_path = state.current_file->path.parent_path() / path;
459 // Then calculate relative to the current xinclude_base.
460 path = path_difference(state.xinclude_base, full_path);
463 return quickbook_path(full_path, 0, path);