diff --git a/build.gradle b/build.gradle index f58f226..70fbe68 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ test { publishing { publications { - myPublication(MavenPublication) { + kotlinSimpleXmlRss(MavenPublication) { from components.java //noinspection GroovyAssignabilityCheck artifact sourcesJar diff --git a/gradle/shipkit.gradle b/gradle/shipkit.gradle index 9257f47..a69cc56 100644 --- a/gradle/shipkit.gradle +++ b/gradle/shipkit.gradle @@ -15,7 +15,7 @@ allprojects { user = 'magneticflux' key = System.getenv("BINTRAY_API_KEY") - publications = ['myPublication'] + publications = ['kotlinSimpleXmlRss'] pkg { repo = 'kotlin-simplexml-rss' diff --git a/src/main/kotlin/com/github/magneticflux/rss/Converters.kt b/src/main/kotlin/com/github/magneticflux/rss/Converters.kt index 430c9d0..3e50c01 100644 --- a/src/main/kotlin/com/github/magneticflux/rss/Converters.kt +++ b/src/main/kotlin/com/github/magneticflux/rss/Converters.kt @@ -1,6 +1,10 @@ package com.github.magneticflux.rss import com.github.magneticflux.rss.namespaces.Namespace +import com.github.magneticflux.rss.namespaces.Namespace.ATOM +import com.github.magneticflux.rss.namespaces.Namespace.DEFAULT +import com.github.magneticflux.rss.namespaces.Namespace.ITUNES +import com.github.magneticflux.rss.namespaces.Namespace.XML import org.simpleframework.xml.convert.Converter import org.simpleframework.xml.stream.InputNode import org.simpleframework.xml.stream.OutputNode @@ -15,10 +19,11 @@ internal val fallbackPersister = createRssPersister() */ internal val InputNode.namespace: Namespace? get() { - return when (reference.toLowerCase()) { - Namespace.DEFAULT.reference -> Namespace.DEFAULT - Namespace.ITUNES.reference -> Namespace.ITUNES - Namespace.ATOM.reference -> Namespace.ATOM + return when { + reference.equals(DEFAULT.reference, true) -> DEFAULT + reference.equals(ITUNES.reference, true) -> ITUNES + reference.equals(ATOM.reference, true) -> ATOM + reference.equals(XML.reference, true) -> XML else -> null } } @@ -30,6 +35,13 @@ internal val InputNode.children: Iterator get() { return InputNodeChildIterator(this) } +/** + * Creates an iterator over an InputNode's attributes. + */ +internal val InputNode.allAttributes: Iterator + get() { + return this.attributes.asSequence().map { this.attributes[it] }.iterator() + } /** * Create a child node containing only the given String with the specified namespace. @@ -40,6 +52,21 @@ internal fun OutputNode.createChild(reference: String = "", name: String, value: child.reference = reference } +/** + * Create a child node containing only the given String with the specified namespace. + */ +internal fun OutputNode.createAttribute( + reference: String = "", + name: String, + value: String, + prefix: String? = null +) { + val child = this.setAttribute(name, value) + child.reference = reference + if (prefix != null) + child.namespaces.setReference(reference, prefix) +} + /** * An iterator that iterates over children of an [InputNode]. */ diff --git a/src/main/kotlin/com/github/magneticflux/rss/Persisters.kt b/src/main/kotlin/com/github/magneticflux/rss/Persisters.kt index 708cb66..a897949 100644 --- a/src/main/kotlin/com/github/magneticflux/rss/Persisters.kt +++ b/src/main/kotlin/com/github/magneticflux/rss/Persisters.kt @@ -1,5 +1,11 @@ package com.github.magneticflux.rss +import com.github.magneticflux.rss.namespaces.atom.converters.AtomAuthorConverter +import com.github.magneticflux.rss.namespaces.atom.converters.AtomContributorConverter +import com.github.magneticflux.rss.namespaces.atom.converters.AtomFeedConverter +import com.github.magneticflux.rss.namespaces.atom.elements.AtomAuthor +import com.github.magneticflux.rss.namespaces.atom.elements.AtomContributor +import com.github.magneticflux.rss.namespaces.atom.elements.AtomFeed import com.github.magneticflux.rss.namespaces.itunes.converters.ITunesImageConverter import com.github.magneticflux.rss.namespaces.itunes.converters.ITunesSubCategoryConverter import com.github.magneticflux.rss.namespaces.itunes.converters.ITunesTopLevelCategoryConverter @@ -62,9 +68,14 @@ fun createRssStrategy(): Strategy { this.bind(Enclosure::class.java, EnclosureConverter) this.bind(Guid::class.java, GuidConverter) this.bind(Source::class.java, SourceConverter) + this.bind(ITunesTopLevelCategory::class.java, ITunesTopLevelCategoryConverter) this.bind(ITunesSubCategory::class.java, ITunesSubCategoryConverter) this.bind(ITunesImage::class.java, ITunesImageConverter) + + this.bind(AtomFeed::class.java, AtomFeedConverter) + this.bind(AtomAuthor::class.java, AtomAuthorConverter) + this.bind(AtomContributor::class.java, AtomContributorConverter) }) } @@ -80,4 +91,4 @@ fun createRssMatcher(): Matcher { this.bind(Locale::class.java, LocaleLanguageTransform) this.bind(URL::class.java, URLTransform) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/magneticflux/rss/Transforms.kt b/src/main/kotlin/com/github/magneticflux/rss/Transforms.kt index 0c6c593..b744ac9 100644 --- a/src/main/kotlin/com/github/magneticflux/rss/Transforms.kt +++ b/src/main/kotlin/com/github/magneticflux/rss/Transforms.kt @@ -2,6 +2,7 @@ package com.github.magneticflux.rss import org.simpleframework.xml.transform.Transform import org.threeten.bp.DayOfWeek +import org.threeten.bp.Instant import org.threeten.bp.ZonedDateTime import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatterBuilder @@ -52,6 +53,20 @@ object ZonedDateTimeTransform : Transform { } } +/** + * @author Mitchell Skaggs + * @since 1.2.0 + */ +object ISODateTimeTransform : Transform { + override fun write(value: Instant): String { + return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(value) + } + + override fun read(value: String): Instant { + return Instant.FROM.queryFrom(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(value)) + } +} + /** * @author Mitchell Skaggs * @since 1.0.1 diff --git a/src/main/kotlin/com/github/magneticflux/rss/namespaces/Namespace.kt b/src/main/kotlin/com/github/magneticflux/rss/namespaces/Namespace.kt index 7892468..a05d7ba 100644 --- a/src/main/kotlin/com/github/magneticflux/rss/namespaces/Namespace.kt +++ b/src/main/kotlin/com/github/magneticflux/rss/namespaces/Namespace.kt @@ -16,4 +16,11 @@ sealed class Namespace { object ATOM : Namespace() { const val reference: String = "http://www.w3.org/2005/Atom" } + + /** + * "Note that unlike all other XML namespaces, both the name and the prefix are specified; i.e., if you want XML 1.0 processors to recognize this namespace, you must use the reserved prefix `xml:`." + */ + object XML : Namespace() { + const val reference: String = "http://www.w3.org/XML/1998/namespace" + } } diff --git a/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/converters/AtomAuthorConverter.kt b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/converters/AtomAuthorConverter.kt new file mode 100644 index 0000000..8b04100 --- /dev/null +++ b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/converters/AtomAuthorConverter.kt @@ -0,0 +1,59 @@ +package com.github.magneticflux.rss.namespaces.atom.converters + +import com.github.magneticflux.rss.allAttributes +import com.github.magneticflux.rss.children +import com.github.magneticflux.rss.createChild +import com.github.magneticflux.rss.namespace +import com.github.magneticflux.rss.namespaces.Namespace +import com.github.magneticflux.rss.namespaces.atom.elements.AtomAuthor +import org.simpleframework.xml.convert.Converter +import org.simpleframework.xml.stream.InputNode +import org.simpleframework.xml.stream.OutputNode + +/** + * @author Mitchell Skaggs + * @since 1.2.0 + */ +object AtomAuthorConverter : Converter { + override fun read(node: InputNode): AtomAuthor { + var base: String? = null + var lang: String? = null + + lateinit var name: String + var uri: String? = null + var email: String? = null + + node.allAttributes.forEach { + when (it.namespace) { + Namespace.XML -> { + when (it.name) { + "base" -> base = it.value + "lang" -> lang = it.value + } + } + } + } + + node.children.forEach { + when (it.namespace) { + Namespace.ATOM -> { + when (it.name) { + "name" -> name = it.value + "uri" -> uri = it.value + "email" -> email = it.value + } + } + } + } + + return AtomAuthor(base, lang, name, uri, email) + } + + override fun write(node: OutputNode, value: AtomAuthor) { + val writable = value.toWritable() + + node.createChild(Namespace.ATOM.reference, "name", writable.name) + writable.uri?.let { node.createChild(Namespace.ATOM.reference, "uri", it) } + writable.email?.let { node.createChild(Namespace.ATOM.reference, "email", it) } + } +} diff --git a/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/converters/AtomContributorConverter.kt b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/converters/AtomContributorConverter.kt new file mode 100644 index 0000000..cd8e529 --- /dev/null +++ b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/converters/AtomContributorConverter.kt @@ -0,0 +1,59 @@ +package com.github.magneticflux.rss.namespaces.atom.converters + +import com.github.magneticflux.rss.allAttributes +import com.github.magneticflux.rss.children +import com.github.magneticflux.rss.createChild +import com.github.magneticflux.rss.namespace +import com.github.magneticflux.rss.namespaces.Namespace +import com.github.magneticflux.rss.namespaces.atom.elements.AtomContributor +import org.simpleframework.xml.convert.Converter +import org.simpleframework.xml.stream.InputNode +import org.simpleframework.xml.stream.OutputNode + +/** + * @author Mitchell Skaggs + * @since 1.2.0 + */ +object AtomContributorConverter : Converter { + override fun read(node: InputNode): AtomContributor { + var base: String? = null + var lang: String? = null + + lateinit var name: String + var uri: String? = null + var email: String? = null + + node.allAttributes.forEach { + when (it.namespace) { + Namespace.XML -> { + when (it.name) { + "base" -> base = it.value + "lang" -> lang = it.value + } + } + } + } + + node.children.forEach { + when (it.namespace) { + Namespace.ATOM -> { + when (it.name) { + "name" -> name = it.value + "uri" -> uri = it.value + "email" -> email = it.value + } + } + } + } + + return AtomContributor(base, lang, name, uri, email) + } + + override fun write(node: OutputNode, value: AtomContributor) { + val writable = value.toWritable() + + node.createChild(Namespace.ATOM.reference, "name", writable.name) + writable.uri?.let { node.createChild(Namespace.ATOM.reference, "uri", it) } + writable.email?.let { node.createChild(Namespace.ATOM.reference, "email", it) } + } +} diff --git a/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/converters/AtomFeedConverter.kt b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/converters/AtomFeedConverter.kt new file mode 100644 index 0000000..bba3761 --- /dev/null +++ b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/converters/AtomFeedConverter.kt @@ -0,0 +1,64 @@ +package com.github.magneticflux.rss.namespaces.atom.converters + +import com.github.magneticflux.rss.allAttributes +import com.github.magneticflux.rss.children +import com.github.magneticflux.rss.createAttribute +import com.github.magneticflux.rss.fallbackPersister +import com.github.magneticflux.rss.namespace +import com.github.magneticflux.rss.namespaces.Namespace +import com.github.magneticflux.rss.namespaces.atom.elements.AtomAuthor +import com.github.magneticflux.rss.namespaces.atom.elements.AtomContributor +import com.github.magneticflux.rss.namespaces.atom.elements.AtomFeed +import org.simpleframework.xml.convert.Converter +import org.simpleframework.xml.stream.InputNode +import org.simpleframework.xml.stream.OutputNode + +/** + * @author Mitchell Skaggs + * @since 1.2.0 + */ +object AtomFeedConverter : Converter { + override fun read(node: InputNode): AtomFeed { + var xmlBase: String? = null + var xmlLang: String? = null + + val authors: MutableList = mutableListOf() + val contributors: MutableList = mutableListOf() + + node.allAttributes.forEach { + when (it.namespace) { + Namespace.XML -> { + when (it.name) { + "base" -> xmlBase = it.value + "lang" -> xmlLang = it.value + } + } + } + } + + node.children.forEach { + when (it.namespace) { + Namespace.ATOM -> { + when (it.name) { + "author" -> authors += fallbackPersister.read(AtomAuthor::class.java, it) + "contributor" -> contributors += fallbackPersister.read( + AtomContributor::class.java, + it + ) + } + } + } + } + + return AtomFeed(xmlBase, xmlLang, authors) + } + + override fun write(node: OutputNode, value: AtomFeed) { + val writable = value.toWritable() + + writable.base?.let { node.createAttribute(Namespace.XML.reference, "base", it, "xml") } + writable.lang?.let { node.createAttribute(Namespace.XML.reference, "lang", it, "xml") } + + writable.author.forEach { fallbackPersister.write(it, node) } + } +} diff --git a/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomAuthor.kt b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomAuthor.kt new file mode 100644 index 0000000..a079ead --- /dev/null +++ b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomAuthor.kt @@ -0,0 +1,49 @@ +package com.github.magneticflux.rss.namespaces.atom.elements + +import com.github.magneticflux.rss.namespaces.Namespace.ATOM +import com.github.magneticflux.rss.namespaces.atom.converters.AtomAuthorConverter +import com.github.magneticflux.rss.namespaces.standard.elements.HasReadWrite +import org.simpleframework.xml.Namespace +import org.simpleframework.xml.Root + +/** + * Properties common to all representations of an `` element. + * + * @since 1.2.0 + */ +interface ICommonAtomAuthor : HasReadWrite, AtomPersonConstruct + +/** + * The final RSS view of an `` element. Defaults are used if applicable. + * + * @since 1.2.0 + */ +interface IAtomAuthor : ICommonAtomAuthor { + override fun toReadOnly(): IAtomAuthor = this +} + +/** + * The literal contents of an `` element. Elements with defaults may be omitted or invalid. + * + * @since 1.2.0 + */ +interface IWritableAtomAuthor : ICommonAtomAuthor { + override fun toWritable(): IWritableAtomAuthor = this +} + +/** + * This class represents an Author object in a [AtomFeed]. + * + * @author Mitchell Skaggs + * @since 1.2.0 + * @see AtomAuthorConverter + */ +@Root(name = "author") +@Namespace(reference = ATOM.reference) +data class AtomAuthor( + override val base: String?, + override val lang: String?, + override val name: String, + override val uri: String?, + override val email: String? +) : IAtomAuthor, IWritableAtomAuthor diff --git a/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomCommonAttributes.kt b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomCommonAttributes.kt new file mode 100644 index 0000000..ba1ac7e --- /dev/null +++ b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomCommonAttributes.kt @@ -0,0 +1,55 @@ +package com.github.magneticflux.rss.namespaces.atom.elements + +/** + * Corresponds to `atomCommonAttributes` in RFC4287. + * + * @author Mitchell Skaggs + * @since 1.2.0 + */ +interface AtomCommonAttributes { + val base: String? + val lang: String? +} + +/** + * Corresponds to `atomTextConstruct` in RFC4287. + * + * @author Mitchell Skaggs + * @since 1.2.0 + */ +interface AtomTextConstruct : AtomCommonAttributes { + val type: AtomTextType? +} + +/** + * Corresponds to `atomPlainTextConstruct` in RFC4287. + * + * @author Mitchell Skaggs + * @since 1.2.0 + */ +interface AtomPlainTextConstruct : AtomTextConstruct { + override val type: AtomPlainTextType? + val text: String +} + +/** + * Corresponds to `atomXHTMLTextConstruct` in RFC4287. + * + * @author Mitchell Skaggs + * @since 1.2.0 + */ +interface AtomXHTMLTextConstruct : AtomTextConstruct { + override val type: AtomXHTMLTextType + val text: String +} + +sealed class AtomTextType + +sealed class AtomPlainTextType : AtomTextType() { + object TEXT : AtomPlainTextType() + object HTML : AtomPlainTextType() +} + +sealed class AtomXHTMLTextType : AtomTextType() { + object XHTML : AtomXHTMLTextType() +} diff --git a/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomContributor.kt b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomContributor.kt new file mode 100644 index 0000000..6a02a06 --- /dev/null +++ b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomContributor.kt @@ -0,0 +1,50 @@ +package com.github.magneticflux.rss.namespaces.atom.elements + +import com.github.magneticflux.rss.namespaces.Namespace.ATOM +import com.github.magneticflux.rss.namespaces.atom.converters.AtomAuthorConverter +import com.github.magneticflux.rss.namespaces.standard.elements.HasReadWrite +import org.simpleframework.xml.Namespace +import org.simpleframework.xml.Root + +/** + * Properties common to all representations of a `` element. + * + * @since 1.2.0 + */ +interface ICommonAtomContributor : HasReadWrite, + AtomPersonConstruct + +/** + * The final RSS view of a `` element. Defaults are used if applicable. + * + * @since 1.2.0 + */ +interface IAtomContributor : ICommonAtomContributor { + override fun toReadOnly(): IAtomContributor = this +} + +/** + * The literal contents of a `` element. Elements with defaults may be omitted or invalid. + * + * @since 1.2.0 + */ +interface IWritableAtomContributor : ICommonAtomContributor { + override fun toWritable(): IWritableAtomContributor = this +} + +/** + * This class represents an Author object in a [AtomFeed]. + * + * @author Mitchell Skaggs + * @since 1.2.0 + * @see AtomAuthorConverter + */ +@Root(name = "contributor") +@Namespace(reference = ATOM.reference) +data class AtomContributor( + override val base: String?, + override val lang: String?, + override val name: String, + override val uri: String?, + override val email: String? +) : IAtomContributor, IWritableAtomContributor diff --git a/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomDateConstruct.kt b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomDateConstruct.kt new file mode 100644 index 0000000..e6484f9 --- /dev/null +++ b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomDateConstruct.kt @@ -0,0 +1,13 @@ +package com.github.magneticflux.rss.namespaces.atom.elements + +import org.threeten.bp.Instant + +/** + * Corresponds to `atomPersonConstruct` in RFC4287. + * + * @author Mitchell Skaggs + * @since 1.2.0 + */ +interface AtomDateConstruct : AtomCommonAttributes { + val dateTime: Instant +} diff --git a/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomFeed.kt b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomFeed.kt new file mode 100644 index 0000000..b257bfb --- /dev/null +++ b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomFeed.kt @@ -0,0 +1,49 @@ +package com.github.magneticflux.rss.namespaces.atom.elements + +import com.github.magneticflux.rss.namespaces.Namespace.ATOM +import com.github.magneticflux.rss.namespaces.atom.converters.AtomFeedConverter +import com.github.magneticflux.rss.namespaces.standard.elements.HasReadWrite +import org.simpleframework.xml.Namespace +import org.simpleframework.xml.Root + +/** + * Properties common to all representations of a `` element. + * + * @since 1.2.0 + */ +interface ICommonAtomFeed : HasReadWrite, AtomCommonAttributes { + val author: List +} + +/** + * The final RSS view of a `` element. Defaults are used if applicable. + * + * @since 1.2.0 + */ +interface IAtomFeed : ICommonAtomFeed { + override fun toReadOnly(): IAtomFeed = this +} + +/** + * The literal contents of a `` element. Elements with defaults may be omitted or invalid. + * + * @since 1.2.0 + */ +interface IWritableAtomFeed : ICommonAtomFeed { + override fun toWritable(): IWritableAtomFeed = this +} + +/** + * This class represents an atom:feed. + * + * @author Mitchell Skaggs + * @since 1.2.0 + * @see AtomFeedConverter + */ +@Root(name = "feed") +@Namespace(reference = ATOM.reference) +data class AtomFeed( + override val base: String?, + override val lang: String?, + override val author: List +) : IAtomFeed, IWritableAtomFeed diff --git a/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomPersonConstruct.kt b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomPersonConstruct.kt new file mode 100644 index 0000000..9b6480d --- /dev/null +++ b/src/main/kotlin/com/github/magneticflux/rss/namespaces/atom/elements/AtomPersonConstruct.kt @@ -0,0 +1,13 @@ +package com.github.magneticflux.rss.namespaces.atom.elements + +/** + * Corresponds to `atomPersonConstruct` in RFC4287. + * + * @author Mitchell Skaggs + * @since 1.2.0 + */ +interface AtomPersonConstruct : AtomCommonAttributes { + val name: String + val uri: String? + val email: String? +} diff --git a/src/test/kotlin/com/github/magneticflux/rss/RssTest.kt b/src/test/kotlin/com/github/magneticflux/rss/RssTest.kt index b1030cc..cd6d1f4 100644 --- a/src/test/kotlin/com/github/magneticflux/rss/RssTest.kt +++ b/src/test/kotlin/com/github/magneticflux/rss/RssTest.kt @@ -1,6 +1,7 @@ package com.github.magneticflux.rss import com.github.magneticflux.rss.SampleUtils.getSample +import com.github.magneticflux.rss.namespaces.atom.elements.AtomFeed import com.github.magneticflux.rss.namespaces.itunes.elements.ITunesSubCategory import com.github.magneticflux.rss.namespaces.itunes.elements.ITunesTopLevelCategory import com.github.magneticflux.rss.namespaces.standard.elements.Category @@ -392,5 +393,29 @@ class RssTest : Spek({ } } } + + given("sample_atom", { "$it RSS feed" }) { + val rssFeed = persister.read(AtomFeed::class.java, getSample("$it.xml")) + + on("feed reread") { + val rssText = persister.write(rssFeed) + + println(rssText) + + val rereadRssFeed = persister.read(AtomFeed::class.java, rssText) + + it("should equal original RSS feed read") { + assertThat(rereadRssFeed, equalTo(rssFeed)) + } + + it("should have the same hashCode as original RSS feed read") { + assertThat(rereadRssFeed.hashCode(), equalTo(rssFeed.hashCode())) + } + + it("should have the same String representation as original RSS feed read") { + assertThat(rereadRssFeed.toString(), equalTo(rssFeed.toString())) + } + } + } } }) diff --git a/src/test/resources/com/github/magneticflux/rss/sample_atom.xml b/src/test/resources/com/github/magneticflux/rss/sample_atom.xml new file mode 100644 index 0000000..cb02e2f --- /dev/null +++ b/src/test/resources/com/github/magneticflux/rss/sample_atom.xml @@ -0,0 +1,50 @@ + + + + Mark Pilgrim + http://example.org/ + f8dy@example.com + + dive into mark + + A <em>lot</em> of effort + went into making this effortless + + 2005-07-31T12:29:29Z + tag:example.org,2003:3 + + + Copyright (c) 2003, Mark Pilgrim + + Example Toolkit + + + Atom draft-07 snapshot + + + tag:example.org,2003:3.2397 + 2005-07-31T12:29:29Z + 2003-12-13T08:29:29-04:00 + + Mark Pilgrim + http://example.org/ + f8dy@example.com + + + Sam Ruby + + + Joe Gregorio + + +
+

+ [Update: The Atom draft is finished.] +

+
+
+
+