public static XDoc CleanseHtmlDocument(XDoc html) { if (html.HasName("html")) { html = html.Clone(); // remove <head> and <tail> elements html["head"].RemoveAll(); html["tail"].RemoveAll(); // make sure there is only one body and validate it var mainBody = html["body[not(@target)]"]; if (mainBody.IsEmpty) { html.Elem("body"); mainBody = html["body[not(@target)]"]; } foreach (XDoc body in html["body[@target]"]) { body.Remove(); } ValidateXHtml(mainBody, true, true); } return(html); }
/// <summary> /// Add text to the document. /// </summary> /// <param name="tag">Enclosing tag for the text.</param> /// <param name="mime">Mime type of the enclosed text.</param> /// <param name="xml">The body document to add.</param> /// <returns>Returns the current document instance.</returns> public XAtomBase AddText(string tag, MimeType mime, XDoc xml) { if (mime.Match(MimeType.XHTML)) { Start(tag).Attr("type", "xhtml"); // add content and normalize the root node XDoc added = xml.Clone().Rename("div"); if (added["@xmlns"].IsEmpty) { added.Attr("xmlns", "http://www.w3.org/1999/xhtml"); } Add(added); } else if (mime.Match(MimeType.HTML)) { Start(tag).Attr("type", "html"); // embed HTML as text Value(xml.ToInnerXHtml()); } else { Start(tag).Attr("type", mime.FullType); Add(xml); } // close element End(); return(this); }
/// <summary> /// Clone the current message. /// </summary> /// <returns>A new message instance.</returns> public DreamMessage Clone() { if (!IsCloneable) { throw new InvalidOperationException("The current message cannot be cloned. It is either closed or contains a payload that cannot be duplicated."); } DreamMessage result; if (_noContent) { result = new DreamMessage(Status, Headers); } else if (_doc != null) { result = new DreamMessage(Status, Headers, _doc.Clone()); } else if (_stream == Stream.Null || (_stream != null && _stream.IsStreamMemorized())) { _stream.Position = 0; var copy = new ChunkedMemoryStream((int)_stream.Length); _stream.CopyTo(copy, _stream.Length); _stream.Position = 0; copy.Position = 0; result = new DreamMessage(Status, Headers, ContentType, ContentLength, copy); } else { var bytes = ToBytes(); result = new DreamMessage(Status, Headers, ContentType, bytes); // length may differ for HEAD requests if (bytes.LongLength != ContentLength) { result.Headers.ContentLength = bytes.LongLength; } } if (HasCookies) { result.Cookies.AddRange(Cookies); } return(result); }
public void Adding_same_document_after_first_dispatch_fires_after_normal_delay() { XDoc m1 = new XDoc("deki-event") .Attr("wikiid", "abc") .Elem("channel", "event://abc/deki/pages/create") .Elem("uri", "http://foo/baz") .Elem("content.uri", "") .Elem("revision.uri", "") .Elem("path", "baz") .Elem("previous-path", "bar"); DateTime queueTime1 = DateTime.Now; _queue.Enqueue(m1); Assert.AreEqual(0, _dispatcher.Dispatches.Count); Assert.IsTrue(_dispatcher.ResetEvent.WaitOne(5000, true), "first callback didn't happen"); _dispatcher.ResetEvent.Reset(); Assert.AreEqual(1, _dispatcher.Dispatches.Count); _queue.Enqueue(m1.Clone()); Assert.AreEqual(1, _dispatcher.Dispatches.Count); Assert.IsTrue(_dispatcher.ResetEvent.WaitOne(5000, true), "second callback didn't happen"); Assert.AreEqual(2, _dispatcher.Dispatches.Count); Assert.AreEqual(0, _peekQueue.Count); }
/// <summary> /// Create a new message. /// </summary> /// <param name="status">Http status.</param> /// <param name="headers">Header collection.</param> /// <param name="contentType">Content Mime-Type.</param> /// <param name="doc">Message body.</param> public DreamMessage(DreamStatus status, DreamHeaders headers, MimeType contentType, XDoc doc) { if (doc == null) { throw new ArgumentNullException("doc"); } this.Status = status; this.Headers = new DreamHeaders(headers); // check if document is empty if (doc.IsEmpty) { // we store empty XML documents as text content; it causes less confusion for browsers this.Headers.ContentType = MimeType.TEXT; this.Headers.ContentLength = 0L; _doc = doc; _bytes = new byte[0]; } else { this.Headers.ContentType = contentType ?? MimeType.XML; _doc = doc.Clone(); } }
public static XDoc WebToggle( [DekiScriptParam("content to toggle")] XDoc content, [DekiScriptParam("title to display for toggle (default: \"Show\")", true)] string title, [DekiScriptParam("heading level for title (default: 3)", true)] int?heading, [DekiScriptParam("content toggle speed (one of \"slow\", \"normal\", \"fast\" or milliseconds number; default: instantaneous)", true)] string speed, [DekiScriptParam("hide content initially (default: true)", true)] bool?hidden ) { if (!content["body[not(@target)]"].IsEmpty) { string id = StringUtil.CreateAlphaNumericKey(8); // clone content so we don't modify the original content = content.Clone(); XDoc body = content["body[not(@target)]"]; // add <style> element XDoc head = content["head"]; if (head.IsEmpty) { content.Elem("head"); head = content["head"]; } head.Elem("style", @"h1.web-expand, h2.web-expand, h3.web-expand, h4.web-expand, h5.web-expand, h6.web-expand { cursor: pointer; } .web-expand span.web-expander { padding-right: 20px; background: transparent url('/skins/common/images/nav-parent-open.gif') no-repeat center right; } .web-expanded span.web-expander { background: transparent url('/skins/common/images/nav-parent-docked.gif') no-repeat center right; }"); // set speed if (string.IsNullOrEmpty(speed)) { speed = string.Empty; } else { int millisec; if (int.TryParse(speed, out millisec)) { speed = millisec.ToString(); } else { speed = "'" + speed + "'"; } } // create toggelable content bool hide = hidden ?? true; content .Start("body") .Start("h" + Math.Max(1, Math.Min(heading ?? 3, 6))) .Attr("class", "web-expand" + (hide ? string.Empty : " web-expanded")) .Attr("onclick", "$(this).toggleClass('web-expanded').next('#" + id + "').toggle(" + speed + ")") .Start("span") .Attr("class", "web-expander") .Value(title ?? "Show") .End() .End() .Start("div") .Attr("id", id) .Attr("style", hide ? "display: none;" : string.Empty) .AddNodes(body) .End() .End(); body.Remove(); } return(content); }
public void XmlClone1() { XDoc doc = _doc.Clone(); Test("xml serialization", doc.ToString(), "<doc source=\"http://www.mindtouch.com\">Hello <bold style=\"blinking\">World</bold>!<br /><bold>Cool</bold><span>Ce\u00e7i est \"une\" id\u00e9e</span><struct><name>John</name><last>Doe</last></struct></doc>"); }
public static XDoc GetInstanceSettingsAsDoc(SiteSettingsRetrievalSettings retrieve) { var instance = DekiContext.Current.Instance; string cachekey = retrieve.IncludeHidden ? CACHE_SETTINGSDOC_WITHHIDDEN : CACHE_SETTINGSDOC; XDoc result = instance.Cache.Get <XDoc>(cachekey, null); if (result == null) { Dictionary <string, ConfigValue> config = GetInstanceSettings(); List <KeyValuePair <string, string> > items = new List <KeyValuePair <string, string> >(); lock (config) { foreach (KeyValuePair <string, ConfigValue> entry in config) { if (entry.Value.IsHidden && !retrieve.IncludeHidden) { continue; } // check if overwritten setting was an element int index = entry.Key.LastIndexOf('/'); bool isElement = ((index + 1) < entry.Key.Length) && (entry.Key[index + 1] != '@'); items.Add(new KeyValuePair <string, string>(entry.Key, entry.Value.Value)); if (isElement) { if (entry.Value.IsReadOnly) { // we need to add a 'readonly' attribute items.Add(new KeyValuePair <string, string>(entry.Key + READONLY_SUFFIX, "true")); } if (entry.Value.IsHidden) { // we need to add a 'hidden' attribute items.Add(new KeyValuePair <string, string>(entry.Key + HIDDEN_SUFFIX, "true")); } } } } //Ensure that attributes are after their associated elements to ensure that the #text and the @attribute are part of the same element rather than creating a new element with just the #text //after the attribute. Consider moving this to Dream XDocFactory.From items.Sort((left, right) => StringUtil.CompareInvariant(left.Key, right.Key)); result = XDocFactory.From(items, "config"); var u = DbUtils.CurrentSession.Users_GetByName(UserBL.ANON_USERNAME); var anonDoc = UserBL.GetUserXmlVerbose(u, null, Utils.ShowPrivateUserInfo(u), true, true); result.InsertValueAt(ANONYMOUS_USER, anonDoc.ToJson()); // Add license information var licenseManager = DekiContext.Current.LicenseManager; var licenseDoc = licenseManager.GetLicenseDocument(false).Clone(); licenseDoc.Elem("type", licenseDoc["@type"].Contents); _log.Debug("Added private information."); licenseDoc["support-agreement"].Remove(); licenseDoc["source-license"].Remove(); licenseDoc["license.public"].Remove(); licenseDoc["grants/service-license"].RemoveAll(); var now = DateTime.UtcNow; foreach (var capability in licenseDoc["grants/*[@date.expire]"]) { if (capability["@date.expire"].AsDate < now) { capability.Remove(); } } XDoc originalLicenseElems = result["license"]; XDoc clonedOriginalLicenseElems = XDoc.Empty; if (!originalLicenseElems.IsEmpty) { clonedOriginalLicenseElems = originalLicenseElems.Clone(); originalLicenseElems.Remove(); licenseDoc.AddNodes(clonedOriginalLicenseElems); } result.Start(LICENSE).AddNodes(licenseDoc).End(); AddVersionInfo(result); instance.Cache.Set(cachekey, result.Clone(), DateTime.UtcNow.AddSeconds(60)); } else { // TODO: remove the clone once cached settings come out of IKeyValueCache (i.e. are serialized) // NOTE(cesarn): We need to clone it because we are stripping and adding // elements depending on permissions and what is requested, but we do not // want to change the cached version that contains everything. result = result.Clone(); } if (!retrieve.IncludeAnonymousUser) { result[ANONYMOUS_USER].Remove(); _log.Debug("Removing anonymous's user information."); } if (!retrieve.IncludeLicense) { var mandatoryLicenseInfo = new XDoc(LICENSE).Start("state").Attr("readonly", "true").Value(result[LICENSE_STATE].Contents).End(); if (DekiContext.Current.LicenseManager.LicenseExpiration != DateTime.MaxValue) { mandatoryLicenseInfo.Start("expiration").Attr("readonly", "true").Value(result[LICENSE_EXPIRATION].Contents).End(); } var productKey = LicenseBL.Instance.BuildProductKey(); if (!String.IsNullOrEmpty(productKey)) { mandatoryLicenseInfo.Start("productkey").Attr("readonly", "true").Value(result[LICENSE_PRODUCTKEY].Contents).End(); } result[LICENSE].Remove(); _log.Debug("Removing private information elements"); result.Start(LICENSE).AddNodes(mandatoryLicenseInfo).End(); } result.InsertValueAt("api/@href", DekiContext.Current.Deki.Self.Uri.AsPublicUri().ToString()); return(result); }
/// <summary> /// Add text to the document. /// </summary> /// <param name="tag">Enclosing tag for the text.</param> /// <param name="mime">Mime type of the enclosed text.</param> /// <param name="xml">The body document to add.</param> /// <returns>Returns the current document instance.</returns> public XAtomBase AddText(string tag, MimeType mime, XDoc xml) { if(mime.Match(MimeType.XHTML)) { Start(tag).Attr("type", "xhtml"); // add content and normalize the root node XDoc added = xml.Clone().Rename("div"); if(added["@xmlns"].IsEmpty) { added.Attr("xmlns", "http://www.w3.org/1999/xhtml"); } Add(added); } else if(mime.Match(MimeType.HTML)) { Start(tag).Attr("type", "html"); // embed HTML as text Value(xml.ToInnerXHtml()); } else { Start(tag).Attr("type", mime.FullType); Add(xml); } // close element End(); return this; }
/// <summary> /// Create a new message. /// </summary> /// <param name="status">Http status.</param> /// <param name="headers">Header collection.</param> /// <param name="contentType">Content Mime-Type.</param> /// <param name="doc">Message body.</param> public DreamMessage(DreamStatus status, DreamHeaders headers, MimeType contentType, XDoc doc) { if(doc == null) { throw new ArgumentNullException("doc"); } this.Status = status; this.Headers = new DreamHeaders(headers); // check if document is empty if(doc.IsEmpty) { // we store empty XML documents as text content; it causes less confusion for browsers this.Headers.ContentType = MimeType.TEXT; this.Headers.ContentLength = 0L; _doc = doc; _bytes = new byte[0]; } else { this.Headers.ContentType = contentType ?? MimeType.XML; _doc = doc.Clone(); } }
//--- Methods --- protected override Yield Start(XDoc config, Result result) { yield return Coroutine.Invoke(base.Start, config, new Result()); // set up plug for phpscript that will handle the notifications _emailer = Plug.New(config["uri.emailer"].AsUri); // set up plug deki, so we can validate users _deki = Plug.New(config["uri.deki"].AsUri); // get the apikey, which we will need as a subscription auth token for subscriptions not done on behalf of a user _apikey = config["apikey"].AsText; _cache = new PageChangeCache(_deki.With("apikey", _apikey), TimeSpan.FromSeconds(config["page-cache-ttl"].AsInt ?? 2)); // resource manager for email template string resourcePath = Config["resources-path"].AsText; if(!string.IsNullOrEmpty(resourcePath)) { _resourceManager = new PlainTextResourceManager(Environment.ExpandEnvironmentVariables(resourcePath)); } else { // creating a test resource manager _log.WarnFormat("'resource-path' was not defined in Config, using a test resource manager for email templating"); TestResourceSet testSet = new TestResourceSet(); testSet.Add("Notification.Page.email-subject", "Page Modified"); testSet.Add("Notification.Page.email-header", "The following pages have changed:"); _resourceManager = new PlainTextResourceManager(testSet); } // get persisted subscription storage List<Tuplet<string, List<XDoc>>> allWikiSubs = new List<Tuplet<string, List<XDoc>>>(); Result<DreamMessage> storageCatalog; yield return storageCatalog = Storage.At("subscriptions").GetAsync(); foreach(XDoc wikiSubs in storageCatalog.Value.ToDocument()["folder/name"]) { string wikihost = wikiSubs.AsText; Tuplet<string, List<XDoc>> wikiDoc = new Tuplet<string, List<XDoc>>(wikihost, new List<XDoc>()); allWikiSubs.Add(wikiDoc); Result<DreamMessage> wikiUsers; yield return wikiUsers = Storage.At("subscriptions", wikihost).GetAsync(); foreach(XDoc userDocname in wikiUsers.Value.ToDocument()["file/name"]) { string userFile = userDocname.AsText; if(!userFile.EndsWith(".xml")) { _log.WarnFormat("Found stray file '{0}' in wiki '{1}' store, ignoring", userFile, wikihost); continue; } Result<DreamMessage> userDoc; yield return userDoc = Storage.At("subscriptions", wikihost, userFile).GetAsync(); try { wikiDoc.Item2.Add(userDoc.Value.ToDocument()); } catch(InvalidDataException e) { _log.Error(string.Format("Unable to retrieve subscription store for user {0}/{1}", wikihost, userFile), e); } } } _subscriptions = new SubscriptionManager(Self.Uri.AsServerUri().At("notify"), allWikiSubs); _subscriptions.RecordsChanged += PersistSubscriptions; _subscriptions.SubscriptionsChanged += PushSubscriptionSetUpstream; // set up subscription for pubsub _baseSubscriptionSet = new XDoc("subscription-set") .Elem("uri.owner", Self.Uri.AsServerUri().ToString()) .Start("subscription") .Elem("channel", "event://*/deki/users/*") .Add(DreamCookie.NewSetCookie("service-key", InternalAccessKey, Self.Uri).AsSetCookieDocument) .Start("recipient") .Attr("authtoken", _apikey) .Elem("uri", Self.Uri.AsServerUri().At("updateuser").ToString()) .End() .End(); XDoc subSet = _baseSubscriptionSet.Clone(); foreach(XDoc sub in _subscriptions.Subscriptions) { subSet.Add(sub); } Result<DreamMessage> subscribe; yield return subscribe = PubSub.At("subscribers").PostAsync(subSet); string accessKey = subscribe.Value.ToDocument()["access-key"].AsText; XUri location = subscribe.Value.Headers.Location; Cookies.Update(DreamCookie.NewSetCookie("access-key", accessKey, location), null); _subscriptionLocation = Plug.New(location.AsLocalUri().WithoutQuery()); _log.DebugFormat("set up initial subscription location at {0}", _subscriptionLocation.Uri); // set up notification accumulator queue TimeSpan accumulationMinutes = TimeSpan.FromSeconds(config["accumulation-time"].AsInt ?? 10 * 60); _log.DebugFormat("Initializing queue with {0:0.00} minute accumulation", accumulationMinutes.TotalMinutes); _notificationQueue = new NotificationDelayQueue(accumulationMinutes, SendEmail); result.Return(); }
public static XDoc CleanseHtmlDocument(XDoc html) { if(html.HasName("html")) { html = html.Clone(); // remove <head> and <tail> elements html["head"].RemoveAll(); html["tail"].RemoveAll(); // make sure there is only one body and validate it var mainBody = html["body[not(@target)]"]; if(mainBody.IsEmpty) { html.Elem("body"); mainBody = html["body[not(@target)]"]; } foreach(XDoc body in html["body[@target]"]) { body.Remove(); } ValidateXHtml(mainBody, true, true); } return html; }
//--- Methods --- /// <summary> /// Store a document in the collection. /// </summary> /// <param name="doc">Document to store.</param> /// <param name="force"><see langword="True"/> if the write should proceed even if optimistic locking meta-data indicates the document is older than the document already stored.</param> /// <returns><see langword="True"/> if the action completed successfully.</returns> public bool Put(XDoc doc, bool force) { Map(doc); string docid = doc[_idXPath].AsText; string revisionClause = string.Empty; XDoc revisionAttr = doc["@docstore:revision"]; if (!revisionAttr.IsEmpty) { if (!force) { int?rev = revisionAttr.AsInt; if (rev.HasValue) { int pk = doc["@docstore:id"].AsInt ?? 0; revisionClause = string.Format("AND id = {0} AND revision = {1}", pk, rev.Value); } } // if we have docstore specific tags, we need to remove them before storage, but don't want to alter the doc // that was given to us doc = doc.Clone(); Map(doc); doc["@docstore:revision"].Remove(); doc["@docstore:id"].Remove(); } if (string.IsNullOrEmpty(docid)) { throw new ArgumentException(string.Format("Document does not contain a valid value at '{0}'", _idXPath)); } int rowsAffected = 0; int id = 0; int revision = 0; // try update first, check for rows affected _catalog.NewQuery(string.Format(@" UPDATE {0} SET id = (@id := id), doc = ?DOC, revision = (@revision := revision + 1) WHERE doc_id = ?DOCID {1}; SELECT ROW_COUNT(), @id, @revision;", _name, revisionClause)) .With("DOCID", docid) .With("DOC", doc.ToString()) .Execute(delegate(IDataReader reader) { while (reader.Read()) { rowsAffected = reader.GetInt32(0); if (rowsAffected == 0) { continue; } // Note (arnec): have to fetch as string and convert to int, because @variables in mysql // are already returned as byte arrays representing strings id = Convert.ToInt32(reader.GetString(1)); revision = Convert.ToInt32(reader.GetString(2)); } }); bool wroteData = (rowsAffected > 0); // if there was a revisionClause it's always an update, so we can skip the next block if (string.IsNullOrEmpty(revisionClause) && rowsAffected == 0) { // no row updated, try insert try { id = _catalog.NewQuery(string.Format(@" INSERT INTO {0} (doc_id,doc) VALUES (?DOCID,?VALUE); SELECT last_insert_id();", _name)) .With("DOCID", docid) .With("VALUE", doc.ToString()).ReadAsInt() ?? 0; revision = 1; } catch (Exception e) { // Note: need to do this by reflection magic, because Dream doesn't take DB dependencies at the dll level bool isDuplicate = false; if (StringUtil.EqualsInvariant(e.GetType().ToString(), "MySql.Data.MySqlClient.MySqlException")) { try { int errorNumber = (int)e.GetType().GetProperty("Number").GetValue(e, null); // trap for duplicate key collisions if (errorNumber == 1062) { isDuplicate = true; } } catch { } if (!isDuplicate) { throw; } } } if (id == 0) { // insert failed, try update once more _catalog.NewQuery(string.Format(@" UPDATE {0} SET id = (@id := id), doc = ?DOC, revision = (@revision := revision + 1) WHERE doc_id = ?DOCID; SELECT @id, @revision;", _name)) .With("DOCID", docid) .With("DOC", doc.ToString()) .Execute(delegate(IDataReader reader) { while (reader.Read()) { // Note (arnec): have to fetch as string and convert to int, because @variables in mysql // are already returned as byte arrays representing strings id = Convert.ToInt32(reader.GetString(0)); revision = Convert.ToInt32(reader.GetString(1)); } }); } wroteData = true; } if (wroteData) { _indexer.QueueUpdate(id, revision, doc); } return(wroteData); }
//--- Methods --- /// <summary> /// Store a document in the collection. /// </summary> /// <param name="doc">Document to store.</param> /// <param name="force"><see langword="True"/> if the write should proceed even if optimistic locking meta-data indicates the document is older than the document already stored.</param> /// <returns><see langword="True"/> if the action completed successfully.</returns> public bool Put(XDoc doc, bool force) { Map(doc); string docid = doc[_idXPath].AsText; string revisionClause = string.Empty; XDoc revisionAttr = doc["@docstore:revision"]; if(!revisionAttr.IsEmpty) { if(!force) { int? rev = revisionAttr.AsInt; if(rev.HasValue) { int pk = doc["@docstore:id"].AsInt ?? 0; revisionClause = string.Format("AND id = {0} AND revision = {1}", pk, rev.Value); } } // if we have docstore specific tags, we need to remove them before storage, but don't want to alter the doc // that was given to us doc = doc.Clone(); Map(doc); doc["@docstore:revision"].Remove(); doc["@docstore:id"].Remove(); } if(string.IsNullOrEmpty(docid)) { throw new ArgumentException(string.Format("Document does not contain a valid value at '{0}'", _idXPath)); } int rowsAffected = 0; int id = 0; int revision = 0; // try update first, check for rows affected _catalog.NewQuery(string.Format(@" UPDATE {0} SET id = (@id := id), doc = ?DOC, revision = (@revision := revision + 1) WHERE doc_id = ?DOCID {1}; SELECT ROW_COUNT(), @id, @revision;", _name, revisionClause)) .With("DOCID", docid) .With("DOC", doc.ToString()) .Execute(delegate(IDataReader reader) { while(reader.Read()) { rowsAffected = reader.GetInt32(0); if(rowsAffected == 0) { continue; } // Note (arnec): have to fetch as string and convert to int, because @variables in mysql // are already returned as byte arrays representing strings id = Convert.ToInt32(reader.GetString(1)); revision = Convert.ToInt32(reader.GetString(2)); } }); bool wroteData = (rowsAffected > 0); // if there was a revisionClause it's always an update, so we can skip the next block if(string.IsNullOrEmpty(revisionClause) && rowsAffected == 0) { // no row updated, try insert try { id = _catalog.NewQuery(string.Format(@" INSERT INTO {0} (doc_id,doc) VALUES (?DOCID,?VALUE); SELECT last_insert_id();", _name)) .With("DOCID", docid) .With("VALUE", doc.ToString()).ReadAsInt() ?? 0; revision = 1; } catch(Exception e) { // Note: need to do this by reflection magic, because Dream doesn't take DB dependencies at the dll level bool isDuplicate = false; if(StringUtil.EqualsInvariant(e.GetType().ToString(), "MySql.Data.MySqlClient.MySqlException")) { try { int errorNumber = (int)e.GetType().GetProperty("Number").GetValue(e, null); // trap for duplicate key collisions if(errorNumber == 1062) { isDuplicate = true; } } catch { } if(!isDuplicate) { throw; } } } if(id == 0) { // insert failed, try update once more _catalog.NewQuery(string.Format(@" UPDATE {0} SET id = (@id := id), doc = ?DOC, revision = (@revision := revision + 1) WHERE doc_id = ?DOCID; SELECT @id, @revision;", _name)) .With("DOCID", docid) .With("DOC", doc.ToString()) .Execute(delegate(IDataReader reader) { while(reader.Read()) { // Note (arnec): have to fetch as string and convert to int, because @variables in mysql // are already returned as byte arrays representing strings id = Convert.ToInt32(reader.GetString(0)); revision = Convert.ToInt32(reader.GetString(1)); } }); } wroteData = true; } if(wroteData) { _indexer.QueueUpdate(id, revision, doc); } return wroteData; }
internal static XDoc TruncateTocDepth(XDoc toc, int? depth) { // check if we need to limit the depth if(depth != null) { toc = toc.Clone(); string xpath = "." + "/ol/li".RepeatPattern(Math.Max(0, depth.Value)) + "/ol"; toc[xpath].RemoveAll(); } return toc; }