I regularly use MadCap Flare for the production of technical documentation. Flare is a sophisticated content authoring tool, which stores all its topic and control files using XML. This makes it relatively easy to process the content of the files programmatically, as in the example of CSS class analysis that I described in a previous post.
The Flare software is based on Microsoft’s .NET framework, so the program runs only under Windows. For that reason, this discussion will be restricted to Windows file systems.
In Windows, the “path” to a file consists of a hierarchical list of subfolders beneath a root volume, for example:
c:\MyRoot\MyProject\Content\MyFile.htm
Sometimes, however, it’s convenient to specify a path relative to another location. For example, if the file at:
c:\MyRoot\MyProject\Content\Subsection\MyTopic.htm
contained a link to MyFile.htm
as above, the relative path could be specified as:
..\MyTopic.htm
In the syntax of relative paths, “..
” means “go up one folder level”. Similarly, “.
” means “this folder level”, so .\MyFile.htm
refers to a file that’s in the same folder as the file containing the relative path.
If you’ve ever examined the markup in Flare files, you’ll have noticed that extensive use is made of “relative paths”. For example, a Flare topic may contain a hyperlink to another topic in the same project, such as:
<MadCap:xref href="..\MyTopic.htm">Linked Topic</MadCap:xref>
Similarly, Flare’s Table-Of-Contents (TOC) files (which have .fltoc
extensions) are XML files that contain trees of TocEntry
elements. Each TocEntry
element has a Link
attribute that contains the path to the topic or sub-TOC that appears at that point in the TOC. All the Link
attribute paths start at the project’s Content (for linked topics) or Project (for linked sub-TOCs) folder, so in that sense they are relative paths.
An example of a TocEntry
element would be:
<TocEntry Title="Sample Topic" Link="/Content/Subsection/MyTopic.htm" />
When I’m writing code to process these files (for example to open and examine each topic in a Flare TOC file), I frequently have to convert Flare’s relative paths into absolute paths (because the XDocument.Load()
method, as described in my previous post, will accept only an absolute path), and vice versa if I want to insert a path into a Flare file. Therefore, I’ve found it very useful to create “library” functions in C# to perform these conversions. I can then call the functions AbsolutePathToRelativePath() and RelativePathToAbsolutePath() without having to think again about the details of how to convert from one format to the other.
I’m sure that there are probably similar functions available in other programming languages. For example, I’m told that Python includes a built-in conversion function called os.path.relpath
, which would make it unnecessary to create custom code. Anyway, my experience as a programmer suggests that you can never have too many code samples, so I’m offering my own versions here to add to the available set. I have tested both functions extensively and they do work as listed.
The methods below are designed as static methods for inclusion in a stringUtilities
class. You could place them in any class, or make them standalone functions.
AbsolutePathToRelativePath
This static method converts an absolute file path specified by strTargFilepath
to its equivalent path relative to strRootDir
. strRootDir
must be a directory tree only, and must not include a file name.
For example, if the absolute path strTargFilepath
is:
c:\folder1\folder2\subfolder1\filename.ext
And the root directory strRootDir
is:
c:\folder1\folder2\folder3\folder4
The method returns the relative file path:
..\..\subfolder1\filename.ext
Note that there must be some commonality between the folder tree of strTargFilepath
and strRootDir
. If there is no commonality, then the method just returns strTargFilepath
unchanged.
The path separator character that will be used in the returned relative path is specified by strPreferredSeparator
. The default value is correct for Windows.
using System.IO;
public static string AbsolutePathToRelativePath(string strRootDir, string strTargFilepath, string strPreferredSeparator = "\\")
{
if (strRootDir == null || strTargFilepath == null)
return null;
string[] strSeps = new string[] { strPreferredSeparator };
if (strRootDir.Length == 0 || strTargFilepath.Length == 0)
return strTargFilepath;
// Convert to arrays
string[] strRootFolders = strRootDir.Split(strSeps, StringSplitOptions.None);
string[] strTargFolders = strTargFilepath.Split(strSeps, StringSplitOptions.None);
if (string.Compare(strRootFolders[0], strTargFolders[0], StringComparison.OrdinalIgnoreCase) != 0)
return strTargFilepath;
// Count common root folders
int i = 0;
List<string> listRelFolders = new List<string>();
for (i = 0; i < strRootFolders.Length; i++)
{
if (string.Compare(strRootFolders[i], strTargFolders[i], StringComparison.OrdinalIgnoreCase) != 0)
break;
}
for (int k = i; k < strTargFolders.Length; k++)
listRelFolders.Add(strTargFolders[k]);
System.Text.StringBuilder sb = new System.Text.StringBuilder();
if (i > 0)
{
// Note: the last element of strTargFolders is actually the filename, so must adjust count for that
for (int j = 0; j < strRootFolders.Length - i; j++)
{
sb.Append("..");
sb.Append(strPreferredSeparator);
}
}
return sb.Append(string.Join(strPreferredSeparator, listRelFolders.ToArray())).ToString();
}
RelativePathToAbsolutePath
This static method converts a relative file path specified by strTargFilepath
to its equivalent absolute path using strRootDir
. strRootDir
must be a directory tree only, and must not include a file name.
For example, if the relative path strTargFilepath
is:
..\..\subfolder1\filename.ext
And the root directory strRootDir
is:
c:\folder1\folder2\folder3\folder4
The method returns the absolute file path:
c:\folder1\folder2\subfolder1\filename.ext
If strTargFilepath
starts with “.\
” or “\
”, then strTargFilepath
is simply appended to strRootDir
The path separator character that will be used in the returned relative path is specified by strPreferredSeparator
. The default value is correct for Windows.
using System.IO;
public static string RelativePathToAbsolutePath(string strRootDir, string strTargFilepath, string strPreferredSeparator = "\\")
{
if (string.IsNullOrEmpty(strRootDir) || string.IsNullOrEmpty(strTargFilepath))
return null;
string[] strSeps = new string[] { strPreferredSeparator };
// Convert to lists
List<string> listTargFolders = strTargFilepath.Split(strSeps, StringSplitOptions.None).ToList<string>();
List<string> listRootFolders = strRootDir.Split(strSeps, StringSplitOptions.None).ToList<string>();
// If strTargFilepath starts with .\ or \, delete initial item
if (string.IsNullOrEmpty(listTargFolders[0]) || (listTargFolders[0] == "."))
listTargFolders.RemoveAt(0);
while (listTargFolders[0] == "..")
{
listRootFolders.RemoveAt(listRootFolders.Count - 1);
listTargFolders.RemoveAt(0);
}
if ((listRootFolders.Count == 0) || (listTargFolders.Count == 0))
return null;
// Combine root and subfolders
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach (string str in listRootFolders)
{
sb.Append(str);
sb.Append(strPreferredSeparator);
}
for (int i = 0; i < listTargFolders.Count; i++)
{
sb.Append(listTargFolders[i]);
if (i < listTargFolders.Count - 1)
sb.Append(strPreferredSeparator);
}
return sb.ToString();
}
[7/1/16] Note that the method above does not check for the case where a relative path contains a partial overlap with the specified absolute path. If required, you would need to add code to handle such cases.
For example, if the relative path strTargFilepath
is:
folder4\subfolder1\filename.ext
and the root directory strRootDir
is:
c:\folder1\folder2\folder3\folder4
the method will not detect that folder4
is actually already part of the root path.