public override void Process(ProcessingStage stage)
        {
            if (!Plugin.Enable)
            {
                return;
            }

            if (stage == ProcessingStage.BeforeLoadingContent)
            {
                _urlSet = new SitemapUrlSet();
            }
            else if (stage == ProcessingStage.BeforeProcessingContent)
            {
                const string ns            = "http://www.sitemaps.org/schemas/sitemap/0.9";
                var          xmlSerializer = new XmlSerializer(typeof(SitemapUrlSet), ns);
                var          namespaces    = new XmlSerializerNamespaces();
                namespaces.Add("", ns);

                var sitemap = new Utf8StringWriter();
                xmlSerializer.Serialize(sitemap, _urlSet, namespaces);

                // Generate sitemap.xml
                var content = new DynamicContentObject(Site, "/sitemap.xml")
                {
                    ContentType = ContentType.Xml,
                    Content     = sitemap.ToString()
                };
                Site.DynamicPages.Add(content);

                // Generate robots.txt
                var robotsContent = new DynamicContentObject(Site, "/robots.txt")
                {
                    ContentType = ContentType.Txt,
                    Content     = $"Sitemap: {Site.Builtins.UrlRef((ContentObject)null, content.Url)}"
                };
                Site.DynamicPages.Add(robotsContent);
            }
        }
        public override void Process(ProcessingStage stage)
        {
            Debug.Assert(stage == ProcessingStage.BeforeProcessingContent);

            foreach (var taxonomy in List.ScriptObject)
            {
                var name  = taxonomy.Key;
                var value = taxonomy.Value;

                string       singular = null;
                string       url      = null;
                ScriptObject map      = null;
                switch (value)
                {
                case string valueAsStr:
                    singular = valueAsStr;
                    break;

                case ScriptObject valueAsObj:
                    singular = valueAsObj.GetSafeValue <string>("singular");
                    url      = valueAsObj.GetSafeValue <string>("url");
                    map      = valueAsObj.GetSafeValue <ScriptObject>("map");
                    break;

                case IScriptCustomFunction _:
                    // Skip functions (clear...etc.)
                    continue;
                }

                if (string.IsNullOrWhiteSpace(singular))
                {
                    // Don't log an error, as we just want to
                    Site.Error($"Invalid singular form [{singular}] of taxonomy [{name}]. Expecting a non empty string");
                    continue;
                }
                // TODO: verify that plural is a valid identifier

                var tax = Find(name);
                if (tax != null)
                {
                    continue;
                }

                List.Add(new Taxonomy(this, name, singular, url, map));
            }

            // Convert taxonomies to readonly after initialization
            List.ScriptObject.Clear();
            foreach (var taxonomy in List)
            {
                List.ScriptObject.SetValue(taxonomy.Name, taxonomy, true);
            }

            foreach (var page in Site.Pages)
            {
                var dyn = (DynamicObject)page;
                foreach (var tax in List)
                {
                    var termsObj = dyn[tax.Name];
                    var terms    = termsObj as ScriptArray;
                    if (termsObj == null)
                    {
                        continue;
                    }
                    if (terms == null)
                    {
                        Site.Error("Invalid type");
                        continue;
                    }

                    foreach (var termNameObj in terms)
                    {
                        var termName = termNameObj as string;
                        if (termName == null)
                        {
                            Site.Error("// TODO ERROR ON TERM");
                            continue;
                        }

                        object       termObj;
                        TaxonomyTerm term;
                        if (!tax.Terms.TryGetValue(termName, out termObj))
                        {
                            termObj             = term = new TaxonomyTerm(tax, termName);
                            tax.Terms[termName] = termObj;
                        }
                        else
                        {
                            term = (TaxonomyTerm)termObj;
                        }

                        term.Pages.Add(page);
                    }
                }
            }

            // Update taxonomy computed
            foreach (var tax in List)
            {
                tax.Update();
            }

            // Generate taxonomy pages
            foreach (var tax in List)
            {
                UPath.TryParse(tax.Url, out var taxPath);
                var section = taxPath.GetFirstDirectory(out var pathInSection);

                bool hasTerms = false;
                // Generate a term page for each term in the current taxonomy
                foreach (var term in tax.Terms.Values.OfType <TaxonomyTerm>())
                {
                    // term.Url
                    var content = new DynamicContentObject(Site, term.Url, section)
                    {
                        ScriptObjectLocal = new ScriptObject(),  // only used to let layout processor running
                        Layout            = tax.Name,
                        LayoutType        = "term",
                        ContentType       = ContentType.Html
                    };

                    content.ScriptObjectLocal.SetValue("pages", term.Pages, true);
                    content.ScriptObjectLocal.SetValue("taxonomy", tax, true);
                    content.ScriptObjectLocal.SetValue("term", term, true);

                    foreach (var page in term.Pages)
                    {
                        content.Dependencies.Add(new PageContentDependency(page));
                    }

                    content.Initialize();

                    Site.DynamicPages.Add(content);
                    hasTerms = true;
                }

                // Generate a terms page for the current taxonomy
                if (hasTerms)
                {
                    var content = new DynamicContentObject(Site, tax.Url, section)
                    {
                        ScriptObjectLocal = new ScriptObject(), // only used to let layout processor running
                        Layout            = tax.Name,
                        LayoutType        = "terms",
                        ContentType       = ContentType.Html
                    };
                    content.ScriptObjectLocal.SetValue("taxonomy", tax, true);
                    content.Initialize();

                    // TODO: Add dependencies

                    Site.DynamicPages.Add(content);
                }
            }
        }
        private void ProcessBundleLinks(BundleObject bundle, Dictionary <UPath, ContentObject> staticFiles)
        {
            Dictionary <string, ConcatGroup> concatBuilders = null;

            if (bundle.Concat)
            {
                concatBuilders = new Dictionary <string, ConcatGroup>();
                foreach (var type in bundle.UrlDestination)
                {
                    if (!concatBuilders.ContainsKey(type.Key))
                    {
                        concatBuilders[type.Key] = new ConcatGroup(type.Key);
                    }
                }
            }

            // Expand wildcard * links
            for (int i = 0; i < bundle.Links.Count; i++)
            {
                var link = bundle.Links[i];
                var path = link.Path;
                var url  = link.Url;
                if (!path.Contains("*"))
                {
                    continue;
                }

                // Always remove the link
                bundle.Links.RemoveAt(i);

                var upath = (UPath)path;

                bool matchingInFileSystem = false;
                foreach (var file in Site.FileSystem.EnumerateFileSystemEntries(upath.GetDirectory(), upath.GetName()))
                {
                    var newLink = new BundleLink(bundle, link.Type, (string)file.Path, url + file.Path.GetName(), link.Mode);
                    bundle.Links.Insert(i++, newLink);
                    matchingInFileSystem = true;
                }

                if (!matchingInFileSystem)
                {
                    foreach (var file in Site.MetaFileSystem.EnumerateFileSystemEntries(upath.GetDirectory(), upath.GetName()))
                    {
                        var newLink = new BundleLink(bundle, link.Type, (string)file.Path, url + file.Path.GetName(), link.Mode);
                        bundle.Links.Insert(i++, newLink);
                    }
                }

                // Cancel the double i++
                i--;
            }

            // Collect minifier
            IContentMinifier minifier = null;

            if (bundle.Minify)
            {
                var minifierName = bundle.Minifier;
                foreach (var min in Minifiers)
                {
                    if (minifierName == null || min.Name == minifierName)
                    {
                        minifier = min;
                        break;
                    }
                }

                if (minifier == null)
                {
                    Site.Warning($"Minify is setup for bundle [{bundle.Name}] but no minifiers are registered (Minified requested: {minifierName ?? "default"})");
                }
            }

            // Process links
            for (int i = 0; i < bundle.Links.Count; i++)
            {
                var link = bundle.Links[i];
                var path = link.Path;
                var url  = link.Url;
                if (url != null)
                {
                    if (!UPath.TryParse(url, out _))
                    {
                        Site.Error($"Invalid absolute url [{url}] in bundle [{bundle.Name}]");
                    }
                }

                if (path != null)
                {
                    path      = ((UPath)path).FullName;
                    link.Path = path;

                    ContentObject currentContent;
                    var           isExistingContent = staticFiles.TryGetValue(path, out currentContent);

                    if (url == null)
                    {
                        var outputUrlDirectory = bundle.UrlDestination[link.Type];
                        // If the file is private or meta, we need to copy to the output
                        // bool isFilePrivateOrMeta = Site.IsFilePrivateOrMeta(entry.FullName);
                        url      = outputUrlDirectory + Path.GetFileName(path);
                        link.Url = url;
                    }

                    FileEntry entry = null;

                    // Process file by existing processors
                    if (currentContent == null)
                    {
                        // Check first site, then meta
                        entry = new FileEntry(Site.FileSystem, path);
                        if (!entry.Exists)
                        {
                            entry = new FileEntry(Site.MetaFileSystem, path);
                        }

                        if (entry.Exists)
                        {
                            currentContent = new FileContentObject(Site, entry);
                        }
                        else
                        {
                            Site.Error($"Unable to find content [{path}] in bundle [{bundle.Name}]");
                        }
                    }

                    if (currentContent != null)
                    {
                        currentContent.Url = url;

                        var listTemp = new PageCollection()
                        {
                            currentContent
                        };
                        Site.Content.ProcessPages(listTemp, false);
                        link.ContentObject = currentContent;

                        bool isRawContent = link.Type == BundleObjectProperties.ContentType;

                        // If we require concat and/or minify, we preload the content of the file
                        if (!isRawContent && (bundle.Concat || bundle.Minify))
                        {
                            try
                            {
                                link.Content = currentContent.Content ?? entry.ReadAllText();

                                // Minify content separately
                                if (bundle.Minify && minifier != null)
                                {
                                    Minify(minifier, link, bundle.MinifyExtension);
                                }
                            }
                            catch (Exception ex)
                            {
                                Site.Error($"Unable to load content [{path}] while trying to concatenate for bundle [{bundle.Name}]. Reason: {ex.GetReason()}");
                            }
                        }

                        // Remove sourcemaps (TODO: make this configurable)
                        RemoveSourceMaps(link);

                        // If we are concatenating
                        if (!isRawContent && concatBuilders != null)
                        {
                            currentContent.Discard = true;

                            // Remove this link from the list of links, as we are going to squash them after
                            bundle.Links.RemoveAt(i);
                            i--;

                            concatBuilders[link.Type].Pages.Add(currentContent);
                            concatBuilders[link.Type].AppendContent(link.Mode, link.Content);
                        }
                        else if (!isExistingContent)
                        {
                            Site.StaticFiles.Add(currentContent);
                            staticFiles.Add(path, currentContent);
                        }
                    }
                }
            }

            // Concatenate files if necessary
            if (concatBuilders != null)
            {
                foreach (var builderGroup in concatBuilders)
                {
                    foreach (var contentPerMode in builderGroup.Value.ModeToBuilders)
                    {
                        var mode    = contentPerMode.Key;
                        var builder = contentPerMode.Value;
                        if (builder.Length > 0)
                        {
                            var type = builderGroup.Key;
                            var outputUrlDirectory = bundle.UrlDestination[type];

                            // If the file is private or meta, we need to copy to the output
                            // bool isFilePrivateOrMeta = Site.IsFilePrivateOrMeta(entry.FullName);
                            var url           = outputUrlDirectory + bundle.Name + (string.IsNullOrEmpty(mode) ? $".{type}" : $"-{mode}.{type}");
                            var newStaticFile = new DynamicContentObject(Site, url)
                            {
                                Content = builder.ToString()
                            };
                            Site.DynamicPages.Add(newStaticFile);

                            // Add file dependencies
                            foreach (var page in builderGroup.Value.Pages)
                            {
                                newStaticFile.Dependencies.Add(new PageContentDependency(page));
                            }

                            var link = new BundleLink(bundle, type, null, url, mode)
                            {
                                Content       = newStaticFile.Content,
                                ContentObject = newStaticFile
                            };

                            bundle.Links.Add(link);
                        }
                    }
                }
            }

            foreach (var link in bundle.Links)
            {
                var contentObject = link.ContentObject;
                if (contentObject != null)
                {
                    link.Url = contentObject.Url;
                }
            }
        }