about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/database/postgres/mod.rs4
-rw-r--r--src/indieauth/mod.rs11
-rw-r--r--src/indieauth/webauthn.rs5
-rw-r--r--src/lib.rs4
-rw-r--r--src/main.rs23
-rw-r--r--src/media/mod.rs8
-rw-r--r--src/micropub/mod.rs34
-rw-r--r--src/webmentions/check.rs23
-rw-r--r--src/webmentions/check/rcdom.rs515
9 files changed, 580 insertions, 47 deletions
diff --git a/src/database/postgres/mod.rs b/src/database/postgres/mod.rs
index 0ebaffb..7f788a8 100644
--- a/src/database/postgres/mod.rs
+++ b/src/database/postgres/mod.rs
@@ -1,5 +1,4 @@
 use std::borrow::Cow;
-use std::str::FromStr;
 
 use kittybox_util::{MicropubChannel, MentionType};
 use sqlx::{ConnectOptions, Executor, PgPool};
@@ -30,6 +29,7 @@ impl From<sqlx::migrate::MigrateError> for StorageError {
     }
 }
 
+/// Micropub storage that uses a PostgreSQL database.
 #[derive(Debug, Clone)]
 pub struct PostgresStorage {
     db: PgPool
@@ -38,7 +38,7 @@ pub struct PostgresStorage {
 impl PostgresStorage {
     /// Construct a [`PostgresStorage`] from a [`sqlx::PgPool`],
     /// running appropriate migrations.
-    pub async fn from_pool(db: sqlx::PgPool) -> Result<Self> {
+    pub(crate) async fn from_pool(db: sqlx::PgPool) -> Result<Self> {
         db.execute(sqlx::query("CREATE SCHEMA IF NOT EXISTS kittybox")).await?;
         MIGRATOR.run(&db).await?;
         Ok(Self { db })
diff --git a/src/indieauth/mod.rs b/src/indieauth/mod.rs
index 2550df0..de4c367 100644
--- a/src/indieauth/mod.rs
+++ b/src/indieauth/mod.rs
@@ -1,13 +1,13 @@
 use std::marker::PhantomData;
-
 use microformats::types::Class;
 use tracing::error;
 use serde::Deserialize;
 use axum::{
-    extract::{Form, FromRef, Host, Json, Query, State}, headers::{authorization::Bearer, Authorization}, http::StatusCode, response::{Html, IntoResponse, Response}, Extension, TypedHeader
+    extract::{Form, FromRef, Host, Json, Query, State}, http::StatusCode, response::{Html, IntoResponse, Response}, Extension
 };
 #[cfg_attr(not(feature = "webauthn"), allow(unused_imports))]
 use axum_extra::extract::cookie::{CookieJar, Cookie};
+use axum_extra::{TypedHeader, headers::{authorization::Bearer, Authorization}};
 use crate::database::Storage;
 use kittybox_indieauth::{
     Metadata, IntrospectionEndpointAuthMethod, RevocationEndpointAuthMethod,
@@ -17,7 +17,6 @@ use kittybox_indieauth::{
     TokenIntrospectionRequest, TokenIntrospectionResponse, TokenRevocationRequest, TokenData
 };
 use std::str::FromStr;
-use std::ops::Deref;
 
 pub mod backend;
 #[cfg(feature = "webauthn")]
@@ -174,7 +173,7 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
 
                         mf2.items
                             .iter()
-                            .find(|&i| (**i).borrow().r#type.iter()
+                            .find(|&i| i.r#type.iter()
                                 .any(|i| {
                                     *i == Class::from_str("h-app").unwrap()
                                         || *i == Class::from_str("h-x-app").unwrap()
@@ -182,7 +181,7 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
                             )
                             .cloned()
                             .map(|i| {
-                                serde_json::to_value(i.borrow().deref()).unwrap()
+                                serde_json::to_value(&i).unwrap()
                             })
                     },
                     Err(err) => {
@@ -315,7 +314,7 @@ async fn authorization_endpoint_confirm<A: AuthBackend>(
     (StatusCode::NO_CONTENT,
      [("Location", location.as_str())],
      #[cfg(feature = "webauthn")]
-     cookies.remove(Cookie::named(webauthn::CHALLENGE_ID_COOKIE))
+     cookies.remove(Cookie::from(webauthn::CHALLENGE_ID_COOKIE))
     )
         .into_response()
 }
diff --git a/src/indieauth/webauthn.rs b/src/indieauth/webauthn.rs
index ea3ad3d..b7d8c71 100644
--- a/src/indieauth/webauthn.rs
+++ b/src/indieauth/webauthn.rs
@@ -1,9 +1,10 @@
 use axum::{
     extract::{Json, Host},
     response::{IntoResponse, Response},
-    http::StatusCode, Extension, TypedHeader, headers::{authorization::Bearer, Authorization}
+    http::StatusCode, Extension
 };
 use axum_extra::extract::cookie::{CookieJar, Cookie};
+use axum_extra::{TypedHeader, headers::{authorization::Bearer, Authorization}};
 
 use super::backend::AuthBackend;
 use crate::database::Storage;
@@ -66,7 +67,7 @@ pub async fn webauthn_pre_register<A: AuthBackend, D: Storage + 'static>(
     match auth.persist_registration_challenge(&uid_url, state).await {
         Ok(challenge_id) => (
             cookies.add(
-                Cookie::build(CHALLENGE_ID_COOKIE, challenge_id)
+                Cookie::build((CHALLENGE_ID_COOKIE, challenge_id))
                     .secure(true)
                     .finish()
             ),
diff --git a/src/lib.rs b/src/lib.rs
index 2d15423..495591d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,10 +7,10 @@ use axum::extract::FromRef;
 use axum_extra::extract::cookie::Key;
 use database::{FileStorage, PostgresStorage, Storage};
 use indieauth::backend::{AuthBackend, FileBackend as FileAuthBackend};
-use kittybox_util::queue::{JobItem, JobQueue};
+use kittybox_util::queue::JobQueue;
 use media::storage::{MediaStore, file::FileStore as FileMediaStore};
 use tokio::{sync::Mutex, task::JoinSet};
-use webmentions::queue::{PostgresJobItem, PostgresJobQueue};
+use webmentions::queue::PostgresJobQueue;
 
 /// Database abstraction layer for Kittybox, allowing the CMS to work with any kind of database.
 pub mod database;
diff --git a/src/main.rs b/src/main.rs
index 4af8a81..f683c38 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,7 @@
 use base64::Engine;
 use kittybox::{database::Storage, indieauth::backend::AuthBackend, media::storage::MediaStore, webmentions::Webmention, compose_kittybox};
 use tokio::{sync::Mutex, task::JoinSet};
-use std::{env, time::Duration, sync::Arc};
+use std::{env, future::IntoFuture, sync::Arc};
 use tracing::error;
 
 
@@ -208,7 +208,7 @@ async fn main() {
         }
     };
 
-    let mut servers: Vec<hyper::server::Server<hyper::server::conn::AddrIncoming, _>> = vec![];
+    let mut servers: Vec<axum::serve::Serve<_, _>> = vec![];
 
     let build_hyper = |tcp: std::net::TcpListener| {
         tracing::info!("Listening on {}", tcp.local_addr().unwrap());
@@ -216,10 +216,14 @@ async fn main() {
         // properly -- this is the async magic!
         tcp.set_nonblocking(true).unwrap();
 
-        hyper::server::Server::from_tcp(tcp).unwrap()
-            // Otherwise Chrome keeps connections open for too long
-            .tcp_keepalive(Some(Duration::from_secs(30 * 60)))
-            .serve(router.clone().into_make_service())
+        //hyper::server::Server::from_tcp(tcp).unwrap()
+        //    // Otherwise Chrome keeps connections open for too long
+        //    .tcp_keepalive(Some(Duration::from_secs(30 * 60)))
+        //    .serve(router.clone().into_make_service())
+        axum::serve(
+            tokio::net::TcpListener::from_std(tcp).unwrap(),
+            router.clone()
+        )
     };
 
     let mut listenfd = listenfd::ListenFd::from_env();
@@ -286,19 +290,20 @@ async fn main() {
         .map(
             #[cfg(not(tokio_unstable))] |server| tokio::task::spawn(
                 server.with_graceful_shutdown(cancellation_token.clone().cancelled_owned())
+                    .into_future()
             ),
             #[cfg(tokio_unstable)] |server| {
                 tokio::task::Builder::new()
-                    .name(format!("Kittybox HTTP acceptor: {}", server.local_addr()).as_str())
+                    .name(format!("Kittybox HTTP acceptor: {:?}", server).as_str())
                     .spawn(
                         server.with_graceful_shutdown(
                             cancellation_token.clone().cancelled_owned()
-                        )
+                        ).into_future()
                     )
                     .unwrap()
             }
         )
-        .collect::<futures_util::stream::FuturesUnordered<tokio::task::JoinHandle<Result<(), hyper::Error>>>>()
+        .collect::<futures_util::stream::FuturesUnordered<tokio::task::JoinHandle<Result<(), std::io::Error>>>>()
     );
 
     #[cfg(not(unix))]
diff --git a/src/media/mod.rs b/src/media/mod.rs
index 47f456a..7884ef8 100644
--- a/src/media/mod.rs
+++ b/src/media/mod.rs
@@ -1,6 +1,8 @@
 use axum::{
-    extract::{multipart::Multipart, FromRef, Host, Path, State}, headers::{HeaderMapExt, HeaderValue, IfNoneMatch}, response::{IntoResponse, Response}, TypedHeader
+    extract::{multipart::Multipart, FromRef, Host, Path, State}, response::{IntoResponse, Response}
 };
+use axum_extra::headers::{HeaderMapExt, HeaderValue, IfNoneMatch};
+use axum_extra::TypedHeader;
 use kittybox_util::error::{MicropubError, ErrorType};
 use kittybox_indieauth::Scope;
 use crate::indieauth::{backend::AuthBackend, User};
@@ -74,7 +76,7 @@ pub(crate) async fn serve<S: MediaStore>(
             tracing::debug!("Metadata: {:?}", metadata);
 
             let etag = if let Some(etag) = metadata.etag {
-                let etag = format!("\"{}\"", etag).parse::<axum::headers::ETag>().unwrap();
+                let etag = format!("\"{}\"", etag).parse::<axum_extra::headers::ETag>().unwrap();
 
                 if let Some(TypedHeader(if_none_match)) = if_none_match {
                     tracing::debug!("If-None-Match: {:?}", if_none_match);
@@ -110,7 +112,7 @@ pub(crate) async fn serve<S: MediaStore>(
                     headers.typed_insert(etag);
                 }
             }
-            r.body(axum::body::StreamBody::new(stream))
+            r.body(axum::body::Body::from_stream(stream))
                 .unwrap()
                 .into_response()
         },
diff --git a/src/micropub/mod.rs b/src/micropub/mod.rs
index fc5dd10..63b81c5 100644
--- a/src/micropub/mod.rs
+++ b/src/micropub/mod.rs
@@ -5,12 +5,12 @@ use std::sync::Arc;
 use crate::database::{MicropubChannel, Storage, StorageError};
 use crate::indieauth::backend::AuthBackend;
 use crate::indieauth::User;
-use crate::media::storage::MediaStore;
 use crate::micropub::util::form_to_mf2_json;
-use axum::extract::{BodyStream, FromRef, Host, Query, State};
-use axum::headers::ContentType;
+use axum::extract::{FromRef, Host, Query, State};
+use axum::body::Body as BodyStream;
+use axum_extra::headers::ContentType;
 use axum::response::{IntoResponse, Response};
-use axum::TypedHeader;
+use axum_extra::TypedHeader;
 use axum::http::StatusCode;
 use serde::{Deserialize, Serialize};
 use serde_json::json;
@@ -136,10 +136,10 @@ async fn background_processing<D: 'static + Storage>(
                 // TODO parse link headers
                 let links = response
                     .headers()
-                    .get_all(hyper::http::header::LINK)
+                    .get_all(reqwest::header::LINK)
                     .iter()
                     .cloned()
-                    .collect::<Vec<hyper::http::HeaderValue>>();
+                    .collect::<Vec<reqwest::header::HeaderValue>>();
                 let html = response.text().await;
                 if html.is_err() {
                     return None;
@@ -330,9 +330,9 @@ pub(crate) async fn _post<D: 'static + Storage>(
         IntoResponse::into_response((StatusCode::ACCEPTED, [("Location", uid.as_str())]));
 
     #[cfg(not(tokio_unstable))]
-    jobset.lock().await.spawn(background_processing(db, mf2, http));
+    let _ = jobset.lock().await.spawn(background_processing(db, mf2, http));
     #[cfg(tokio_unstable)]
-    jobset.lock().await.build_task()
+    let _ = jobset.lock().await.build_task()
         .name(format!("Kittybox background processing for post {}", uid.as_str()).as_str())
         .spawn(background_processing(db, mf2, http));
 
@@ -459,7 +459,7 @@ enum PostBody {
 
 #[tracing::instrument]
 async fn dispatch_body(
-    mut body: BodyStream,
+    body: BodyStream,
     content_type: ContentType,
 ) -> Result<PostBody, MicropubError> {
     let body: Vec<u8> = {
@@ -467,6 +467,7 @@ async fn dispatch_body(
         use tokio_stream::StreamExt;
         let mut buf = Vec::default();
 
+        let mut body = body.into_data_stream();
         while let Some(chunk) = body.next().await {
             buf.extend_from_slice(&chunk.unwrap())
         }
@@ -673,7 +674,7 @@ where
 {
     axum::routing::get(query::<S, A>)
         .post(post::<S, A>)
-        .layer::<_, _, std::convert::Infallible>(tower_http::cors::CorsLayer::new()
+        .layer::<_, _>(tower_http::cors::CorsLayer::new()
                .allow_methods([
                    axum::http::Method::GET,
                    axum::http::Method::POST,
@@ -704,7 +705,8 @@ mod tests {
     use std::sync::Arc;
 
     use crate::{database::Storage, micropub::MicropubError};
-    use hyper::body::HttpBody;
+    use bytes::Bytes;
+    use futures::StreamExt;
     use serde_json::json;
     use tokio::sync::Mutex;
 
@@ -861,7 +863,15 @@ mod tests {
         .await;
 
         assert_eq!(res.status(), 401);
-        let body = res.body_mut().data().await.unwrap().unwrap();
+        let body = res
+            .into_body()
+            .into_data_stream()
+            .collect::<Vec<Result<Bytes, axum::Error>>>()
+            .await
+            .into_iter()
+            .map(Result::unwrap)
+            .by_ref()
+            .fold(Vec::new(), |mut a, i| { a.extend(i); a});
         let json: MicropubError = serde_json::from_slice(&body as &[u8]).unwrap();
         assert_eq!(json.error, super::ErrorType::NotAuthorized);
     }
diff --git a/src/webmentions/check.rs b/src/webmentions/check.rs
index 6dc6a25..178c008 100644
--- a/src/webmentions/check.rs
+++ b/src/webmentions/check.rs
@@ -1,7 +1,11 @@
-use std::{cell::RefCell, rc::Rc};
-use microformats::{types::PropertyValue, html5ever::{self, tendril::TendrilSink}};
+use std::rc::Rc;
+use microformats::types::PropertyValue;
+use html5ever::{self, tendril::TendrilSink};
 use kittybox_util::MentionType;
 
+// TODO: replace.
+mod rcdom;
+
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
     #[error("microformats error: {0}")]
@@ -19,19 +23,16 @@ pub fn check_mention(document: impl AsRef<str> + std::fmt::Debug, base_url: &url
     let document = microformats::from_html(document.as_ref(), base_url.clone())?;
 
     // Get an iterator of all items
-    let items_iter = document.items.iter()
-        .map(AsRef::as_ref)
-        .map(RefCell::borrow);
+    let items_iter = document.items.iter();
 
     for item in items_iter {
         tracing::debug!("Processing item: {:?}", item);
 
-        let props = item.properties.borrow();
         for (prop, interaction_type) in [
             ("in-reply-to", MentionType::Reply), ("like-of", MentionType::Like),
             ("bookmark-of", MentionType::Bookmark), ("repost-of", MentionType::Repost)
         ] {
-            if let Some(propvals) = props.get(prop) {
+            if let Some(propvals) = item.properties.get(prop) {
                 tracing::debug!("Has a u-{} property", prop);
                 for val in propvals {
                     if let PropertyValue::Url(url) = val {
@@ -45,13 +46,13 @@ pub fn check_mention(document: impl AsRef<str> + std::fmt::Debug, base_url: &url
         }
         // Process `content`
         tracing::debug!("Processing e-content...");
-        if let Some(PropertyValue::Fragment(content)) = props.get("content")
+        if let Some(PropertyValue::Fragment(content)) = item.properties.get("content")
             .map(Vec::as_slice)
             .unwrap_or_default()
             .first()
         {
             tracing::debug!("Parsing HTML data...");
-            let root = html5ever::parse_document(html5ever::rcdom::RcDom::default(), Default::default())
+            let root = html5ever::parse_document(rcdom::RcDom::default(), Default::default())
                 .from_utf8()
                 .one(content.html.to_owned().as_bytes())
                 .document;
@@ -64,7 +65,7 @@ pub fn check_mention(document: impl AsRef<str> + std::fmt::Debug, base_url: &url
             // iteration of the loop.
             //
             // Empty list means all nodes were processed.
-            let mut unprocessed_nodes: Vec<Rc<html5ever::rcdom::Node>> = root.children.borrow().iter().cloned().collect();
+            let mut unprocessed_nodes: Vec<Rc<rcdom::Node>> = root.children.borrow().iter().cloned().collect();
             while !unprocessed_nodes.is_empty() {
                 // "Take" the list out of its memory slot, replace it with an empty list
                 let nodes = std::mem::take(&mut unprocessed_nodes);
@@ -73,7 +74,7 @@ pub fn check_mention(document: impl AsRef<str> + std::fmt::Debug, base_url: &url
                     // Add children nodes to the list for the next iteration
                     unprocessed_nodes.extend(node.children.borrow().iter().cloned());
 
-                    if let html5ever::rcdom::NodeData::Element { ref name, ref attrs, .. } = node.data {
+                    if let rcdom::NodeData::Element { ref name, ref attrs, .. } = node.data {
                         // If it's not `<a>`, skip it
                         if name.local != *"a" { continue; }
                         let mut is_mention: bool = false;
diff --git a/src/webmentions/check/rcdom.rs b/src/webmentions/check/rcdom.rs
new file mode 100644
index 0000000..549610f
--- /dev/null
+++ b/src/webmentions/check/rcdom.rs
@@ -0,0 +1,515 @@
+// Copyright 2014-2017 The html5ever Project Developers.
+// Copyright Michael Howell and others.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+#![allow(missing_docs)]
+
+//! A simple reference-counted DOM.
+//!
+//! This is sufficient as a static parse tree, but don't build a
+//! web browser using it. :)
+//!
+//! A DOM is a [tree structure] with ordered children that can be represented in an XML-like
+//! format. For example, the following graph
+//!
+//! ```text
+//! div
+//!  +- "text node"
+//!  +- span
+//! ```
+//! in HTML would be serialized as
+//!
+//! ```html
+//! <div>text node<span></span></div>
+//! ```
+//!
+//! See the [document object model article on wikipedia][dom wiki] for more information.
+//!
+//! This implementation stores the information associated with each node once, and then hands out
+//! refs to children. The nodes themselves are reference-counted to avoid copying - you can create
+//! a new ref and then a node will outlive the document. Nodes own their children, but only have
+//! weak references to their parents.
+//!
+//! [tree structure]: https://en.wikipedia.org/wiki/Tree_(data_structure)
+//! [dom wiki]: https://en.wikipedia.org/wiki/Document_Object_Model
+
+use std::borrow::Cow;
+use std::cell::{Cell, RefCell};
+use std::collections::{HashSet, VecDeque};
+use std::default::Default;
+use std::fmt;
+use std::io;
+use std::mem;
+use std::rc::{Rc, Weak};
+
+use html5ever::tendril::StrTendril;
+
+use html5ever::interface::tree_builder;
+use html5ever::interface::tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink};
+use html5ever::serialize::TraversalScope;
+use html5ever::serialize::TraversalScope::{ChildrenOnly, IncludeNode};
+use html5ever::serialize::{Serialize, Serializer};
+use html5ever::Attribute;
+use html5ever::ExpandedName;
+use html5ever::QualName;
+
+/// The different kinds of nodes in the DOM.
+#[derive(Debug)]
+pub enum NodeData {
+    /// The `Document` itself - the root node of a HTML document.
+    Document,
+
+    /// A `DOCTYPE` with name, public id, and system id. See
+    /// [document type declaration on wikipedia][dtd wiki].
+    ///
+    /// [dtd wiki]: https://en.wikipedia.org/wiki/Document_type_declaration
+    Doctype {
+        name: StrTendril,
+        public_id: StrTendril,
+        system_id: StrTendril,
+    },
+
+    /// A text node.
+    Text { contents: RefCell<StrTendril> },
+
+    /// A comment.
+    Comment { contents: StrTendril },
+
+    /// An element with attributes.
+    Element {
+        name: QualName,
+        attrs: RefCell<Vec<Attribute>>,
+
+        /// For HTML \<template\> elements, the [template contents].
+        ///
+        /// [template contents]: https://html.spec.whatwg.org/multipage/#template-contents
+        template_contents: RefCell<Option<Handle>>,
+
+        /// Whether the node is a [HTML integration point].
+        ///
+        /// [HTML integration point]: https://html.spec.whatwg.org/multipage/#html-integration-point
+        mathml_annotation_xml_integration_point: bool,
+    },
+
+    /// A Processing instruction.
+    ProcessingInstruction {
+        target: StrTendril,
+        contents: StrTendril,
+    },
+}
+
+/// A DOM node.
+pub struct Node {
+    /// Parent node.
+    pub parent: Cell<Option<WeakHandle>>,
+    /// Child nodes of this node.
+    pub children: RefCell<Vec<Handle>>,
+    /// Represents this node's data.
+    pub data: NodeData,
+}
+
+impl Node {
+    /// Create a new node from its contents
+    pub fn new(data: NodeData) -> Rc<Self> {
+        Rc::new(Node {
+            data,
+            parent: Cell::new(None),
+            children: RefCell::new(Vec::new()),
+        })
+    }
+}
+
+impl Drop for Node {
+    fn drop(&mut self) {
+        let mut nodes = mem::take(&mut *self.children.borrow_mut());
+        while let Some(node) = nodes.pop() {
+            let children = mem::take(&mut *node.children.borrow_mut());
+            nodes.extend(children.into_iter());
+            if let NodeData::Element {
+                ref template_contents,
+                ..
+            } = node.data
+            {
+                if let Some(template_contents) = template_contents.borrow_mut().take() {
+                    nodes.push(template_contents);
+                }
+            }
+        }
+    }
+}
+
+impl fmt::Debug for Node {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+        fmt.debug_struct("Node")
+            .field("data", &self.data)
+            .field("children", &self.children)
+            .finish()
+    }
+}
+
+/// Reference to a DOM node.
+pub type Handle = Rc<Node>;
+
+/// Weak reference to a DOM node, used for parent pointers.
+pub type WeakHandle = Weak<Node>;
+
+/// Append a parentless node to another nodes' children
+fn append(new_parent: &Handle, child: Handle) {
+    let previous_parent = child.parent.replace(Some(Rc::downgrade(new_parent)));
+    // Invariant: child cannot have existing parent
+    assert!(previous_parent.is_none());
+    new_parent.children.borrow_mut().push(child);
+}
+
+/// If the node has a parent, get it and this node's position in its children
+fn get_parent_and_index(target: &Handle) -> Option<(Handle, usize)> {
+    if let Some(weak) = target.parent.take() {
+        let parent = weak.upgrade().expect("dangling weak pointer");
+        target.parent.set(Some(weak));
+        let i = match parent
+            .children
+            .borrow()
+            .iter()
+            .enumerate()
+            .find(|&(_, child)| Rc::ptr_eq(child, target))
+        {
+            Some((i, _)) => i,
+            None => panic!("have parent but couldn't find in parent's children!"),
+        };
+        Some((parent, i))
+    } else {
+        None
+    }
+}
+
+fn append_to_existing_text(prev: &Handle, text: &str) -> bool {
+    match prev.data {
+        NodeData::Text { ref contents } => {
+            contents.borrow_mut().push_slice(text);
+            true
+        }
+        _ => false,
+    }
+}
+
+fn remove_from_parent(target: &Handle) {
+    if let Some((parent, i)) = get_parent_and_index(target) {
+        parent.children.borrow_mut().remove(i);
+        target.parent.set(None);
+    }
+}
+
+/// The DOM itself; the result of parsing.
+pub struct RcDom {
+    /// The `Document` itself.
+    pub document: Handle,
+
+    /// Errors that occurred during parsing.
+    pub errors: Vec<Cow<'static, str>>,
+
+    /// The document's quirks mode.
+    pub quirks_mode: QuirksMode,
+}
+
+impl TreeSink for RcDom {
+    type Output = Self;
+    fn finish(self) -> Self {
+        self
+    }
+
+    type Handle = Handle;
+
+    fn parse_error(&mut self, msg: Cow<'static, str>) {
+        self.errors.push(msg);
+    }
+
+    fn get_document(&mut self) -> Handle {
+        self.document.clone()
+    }
+
+    fn get_template_contents(&mut self, target: &Handle) -> Handle {
+        if let NodeData::Element {
+            ref template_contents,
+            ..
+        } = target.data
+        {
+            template_contents
+                .borrow()
+                .as_ref()
+                .expect("not a template element!")
+                .clone()
+        } else {
+            panic!("not a template element!")
+        }
+    }
+
+    fn set_quirks_mode(&mut self, mode: QuirksMode) {
+        self.quirks_mode = mode;
+    }
+
+    fn same_node(&self, x: &Handle, y: &Handle) -> bool {
+        Rc::ptr_eq(x, y)
+    }
+
+    fn elem_name<'a>(&self, target: &'a Handle) -> ExpandedName<'a> {
+        return match target.data {
+            NodeData::Element { ref name, .. } => name.expanded(),
+            _ => panic!("not an element!"),
+        };
+    }
+
+    fn create_element(
+        &mut self,
+        name: QualName,
+        attrs: Vec<Attribute>,
+        flags: ElementFlags,
+    ) -> Handle {
+        Node::new(NodeData::Element {
+            name,
+            attrs: RefCell::new(attrs),
+            template_contents: RefCell::new(if flags.template {
+                Some(Node::new(NodeData::Document))
+            } else {
+                None
+            }),
+            mathml_annotation_xml_integration_point: flags.mathml_annotation_xml_integration_point,
+        })
+    }
+
+    fn create_comment(&mut self, text: StrTendril) -> Handle {
+        Node::new(NodeData::Comment { contents: text })
+    }
+
+    fn create_pi(&mut self, target: StrTendril, data: StrTendril) -> Handle {
+        Node::new(NodeData::ProcessingInstruction {
+            target,
+            contents: data,
+        })
+    }
+
+    fn append(&mut self, parent: &Handle, child: NodeOrText<Handle>) {
+        // Append to an existing Text node if we have one.
+        if let NodeOrText::AppendText(ref text) = child {
+            if let Some(h) = parent.children.borrow().last() {
+                if append_to_existing_text(h, text) {
+                    return;
+                }
+            }
+        }
+
+        append(
+            parent,
+            match child {
+                NodeOrText::AppendText(text) => Node::new(NodeData::Text {
+                    contents: RefCell::new(text),
+                }),
+                NodeOrText::AppendNode(node) => node,
+            },
+        );
+    }
+
+    fn append_before_sibling(&mut self, sibling: &Handle, child: NodeOrText<Handle>) {
+        let (parent, i) = get_parent_and_index(sibling)
+            .expect("append_before_sibling called on node without parent");
+
+        let child = match (child, i) {
+            // No previous node.
+            (NodeOrText::AppendText(text), 0) => Node::new(NodeData::Text {
+                contents: RefCell::new(text),
+            }),
+
+            // Look for a text node before the insertion point.
+            (NodeOrText::AppendText(text), i) => {
+                let children = parent.children.borrow();
+                let prev = &children[i - 1];
+                if append_to_existing_text(prev, &text) {
+                    return;
+                }
+                Node::new(NodeData::Text {
+                    contents: RefCell::new(text),
+                })
+            }
+
+            // The tree builder promises we won't have a text node after
+            // the insertion point.
+
+            // Any other kind of node.
+            (NodeOrText::AppendNode(node), _) => node,
+        };
+
+        remove_from_parent(&child);
+
+        child.parent.set(Some(Rc::downgrade(&parent)));
+        parent.children.borrow_mut().insert(i, child);
+    }
+
+    fn append_based_on_parent_node(
+        &mut self,
+        element: &Self::Handle,
+        prev_element: &Self::Handle,
+        child: NodeOrText<Self::Handle>,
+    ) {
+        let parent = element.parent.take();
+        let has_parent = parent.is_some();
+        element.parent.set(parent);
+
+        if has_parent {
+            self.append_before_sibling(element, child);
+        } else {
+            self.append(prev_element, child);
+        }
+    }
+
+    fn append_doctype_to_document(
+        &mut self,
+        name: StrTendril,
+        public_id: StrTendril,
+        system_id: StrTendril,
+    ) {
+        append(
+            &self.document,
+            Node::new(NodeData::Doctype {
+                name,
+                public_id,
+                system_id,
+            }),
+        );
+    }
+
+    fn add_attrs_if_missing(&mut self, target: &Handle, attrs: Vec<Attribute>) {
+        let mut existing = if let NodeData::Element { ref attrs, .. } = target.data {
+            attrs.borrow_mut()
+        } else {
+            panic!("not an element")
+        };
+
+        let existing_names = existing
+            .iter()
+            .map(|e| e.name.clone())
+            .collect::<HashSet<_>>();
+        existing.extend(
+            attrs
+                .into_iter()
+                .filter(|attr| !existing_names.contains(&attr.name)),
+        );
+    }
+
+    fn remove_from_parent(&mut self, target: &Handle) {
+        remove_from_parent(target);
+    }
+
+    fn reparent_children(&mut self, node: &Handle, new_parent: &Handle) {
+        let mut children = node.children.borrow_mut();
+        let mut new_children = new_parent.children.borrow_mut();
+        for child in children.iter() {
+            let previous_parent = child.parent.replace(Some(Rc::downgrade(new_parent)));
+            assert!(Rc::ptr_eq(
+                node,
+                &previous_parent.unwrap().upgrade().expect("dangling weak")
+            ))
+        }
+        new_children.extend(mem::take(&mut *children));
+    }
+
+    fn is_mathml_annotation_xml_integration_point(&self, target: &Handle) -> bool {
+        if let NodeData::Element {
+            mathml_annotation_xml_integration_point,
+            ..
+        } = target.data
+        {
+            mathml_annotation_xml_integration_point
+        } else {
+            panic!("not an element!")
+        }
+    }
+}
+
+impl Default for RcDom {
+    fn default() -> RcDom {
+        RcDom {
+            document: Node::new(NodeData::Document),
+            errors: vec![],
+            quirks_mode: tree_builder::NoQuirks,
+        }
+    }
+}
+
+enum SerializeOp {
+    Open(Handle),
+    Close(QualName),
+}
+
+pub struct SerializableHandle(Handle);
+
+impl From<Handle> for SerializableHandle {
+    fn from(h: Handle) -> SerializableHandle {
+        SerializableHandle(h)
+    }
+}
+
+impl Serialize for SerializableHandle {
+    fn serialize<S>(&self, serializer: &mut S, traversal_scope: TraversalScope) -> io::Result<()>
+    where
+        S: Serializer,
+    {
+        let mut ops = VecDeque::new();
+        match traversal_scope {
+            IncludeNode => ops.push_back(SerializeOp::Open(self.0.clone())),
+            ChildrenOnly(_) => ops.extend(
+                self.0
+                    .children
+                    .borrow()
+                    .iter()
+                    .map(|h| SerializeOp::Open(h.clone())),
+            ),
+        }
+
+        while let Some(op) = ops.pop_front() {
+            match op {
+                SerializeOp::Open(handle) => match handle.data {
+                    NodeData::Element {
+                        ref name,
+                        ref attrs,
+                        ..
+                    } => {
+                        serializer.start_elem(
+                            name.clone(),
+                            attrs.borrow().iter().map(|at| (&at.name, &at.value[..])),
+                        )?;
+
+                        ops.reserve(1 + handle.children.borrow().len());
+                        ops.push_front(SerializeOp::Close(name.clone()));
+
+                        for child in handle.children.borrow().iter().rev() {
+                            ops.push_front(SerializeOp::Open(child.clone()));
+                        }
+                    }
+
+                    NodeData::Doctype { ref name, .. } => serializer.write_doctype(name)?,
+
+                    NodeData::Text { ref contents } => serializer.write_text(&contents.borrow())?,
+
+                    NodeData::Comment { ref contents } => serializer.write_comment(contents)?,
+
+                    NodeData::ProcessingInstruction {
+                        ref target,
+                        ref contents,
+                    } => serializer.write_processing_instruction(target, contents)?,
+
+                    NodeData::Document => panic!("Can't serialize Document node itself"),
+                },
+
+                SerializeOp::Close(name) => {
+                    serializer.end_elem(name)?;
+                }
+            }
+        }
+
+        Ok(())
+    }
+}