Repository: washingtonpost/ans-schema (js)

Issue:
Proposal: Nested blockquotes & credit
## Problem

Blockquotes are frequently used to excerpt large amounts of content from other sources.  Currently the `blockquote` element includes only one exclusive field, `content` which expects an HTML string.  This makes quotes longer than a single paragraph difficult (either embed the parapgraph breaks within the content as HTML, or have a series of separate `blockquote` elements), it makes mixed-content like images, video difficult (the only option is to render it all to HTML), and it makes nested blockquotes -- an actual use case -- difficult for the same reason.  

Furthermore, when rendered, quotes often have a credit or source associated with them. Currently this is not represented in ANS and requires a separate `text` element, not clearly semantically linked.

## Proposed Solution

Add a `quote` element to ANS.  A `quote` will differ from a `blockquote` in two key ways

* `content_elements` attribute -- Instead of a flat `content` string, this will include a list of sub-elements (similar to a `element_group` or sub-story)
* `citation` -- (Optional) A text element for describing the source of the quote

For convenience, `blockquote` will continue to exist for the ANS 0.5.8 in its current form and be deprecated+upverted in a later release

## Example

```
{
  "type": "quote",
  "content_elements": [
    { 
       "type": "text",
       "content": "Now is the time for all good men to come to the aid of their party."
     },
     {
       "type": "image",
       "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/05/Underwoodfive.jpg/220px-Underwoodfive.jpg"
     }
  ],
  "citation": {
    "type": "text",
    "content": "Charles E. Weller; <em>The Early History of the Typewriter</em>, p. 21 (1918)"
   }
}
```

## Potential concerns

* Should any content elements be forbidden from being included in a `quote`'s content elements?


Pull request description:
Adds `quote` element (Fixes #89)

Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 35b70b3..905d191 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,9 +1,15 @@
 # ANS Release Notes
 
+### 1.2.8 2017/01/10 ###
+
+* Adds `quote` element #89
+
 ### 1.2.7 2017/01/09 ###
 
-* Adds `description`, `slug`, `additional_properties` properties to tags
-* Disables non-spec properties on tags
+* Adds `alignment` property on all elements #85
+* Adds `element_group` element #84
+* Adds `description`, `slug`, `additional_properties` properties to tags #87
+* Disables non-spec properties on tags #87
 
 ### 1.2.6 2017/01/05 ###
 
diff --git a/package.json b/package.json
index 2d2ac31..f3877bc 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "ans-schema",
   "description": "The Washington Post's Arc Native Specification",
-  "version": "1.2.7",
+  "version": "1.2.8",
   "homepage": "https://github.com/washingtonpost/ans-schema",
   "repository": {
     "type": "git",
diff --git a/src/main/resources/schema/ans/0.5.8/story_elements/quote.json b/src/main/resources/schema/ans/0.5.8/story_elements/quote.json
new file mode 100644
index 0000000..b001497
--- /dev/null
+++ b/src/main/resources/schema/ans/0.5.8/story_elements/quote.json
@@ -0,0 +1,35 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "id": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.5.8/story_elements/quote.json",
+  "description": "A blockquote content element",
+  "type": "object",
+  "additionalPropeties": false,
+  "properties": {
+    "type": {
+      "enum": [ "quote" ]
+    },
+    "_id": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.5.8/traits/trait_id.json"
+    },
+    "subtype": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.5.8/traits/trait_subtype.json"
+    },
+    "channels": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.5.8/traits/trait_channel.json"
+    },
+    "alignment": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.5.8/traits/trait_alignment.json"
+    },
+    "additional_properties": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.5.8/traits/trait_additional_properties.json"
+    },
+
+    "content_elements": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.5.8/traits/trait_content_elements.json"
+    },
+    "citation": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.5.8/story_elements/text.json"
+    }
+  },
+  "required": [ "content_elements" ]
+}


Test patch:
diff --git a/tests/fixtures/schema/0.5.8/quote-fixture-bad-citation.json b/tests/fixtures/schema/0.5.8/quote-fixture-bad-citation.json
new file mode 100644
index 0000000..33dab64
--- /dev/null
+++ b/tests/fixtures/schema/0.5.8/quote-fixture-bad-citation.json
@@ -0,0 +1,8 @@
+{
+  "type": "quote",
+  "content_elements": [{
+    "type": "text",
+    "content": "Citation should be a text object"
+  }],
+  "citation": "Citation is broken."
+}
diff --git a/tests/fixtures/schema/0.5.8/quote-fixture-bad-content-elements.json b/tests/fixtures/schema/0.5.8/quote-fixture-bad-content-elements.json
new file mode 100644
index 0000000..93f185f
--- /dev/null
+++ b/tests/fixtures/schema/0.5.8/quote-fixture-bad-content-elements.json
@@ -0,0 +1,7 @@
+{
+  "type": "quote",
+  "citation": {
+    "type": "text",
+    "content": "At least the citation is correct!"
+  }
+}
diff --git a/tests/fixtures/schema/0.5.8/story-fixture-good.json b/tests/fixtures/schema/0.5.8/story-fixture-good.json
index 2335020..d513f8f 100644
--- a/tests/fixtures/schema/0.5.8/story-fixture-good.json
+++ b/tests/fixtures/schema/0.5.8/story-fixture-good.json
@@ -268,6 +268,38 @@
       "content": "<i>Here's my html</i>"
 
     },
+    {
+      "_id": "549876543210",
+      "type": "quote",
+      "content_elements": [
+        {
+          "type": "text",
+          "content": "I really like this sample sentence. Maybe we should use it?"
+        },
+        {
+          "type": "quote",
+          "content_elements": [
+            {
+              "type": "image",
+              "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/05/Underwoodfive.jpg/220px-Underwoodfive.jpg"
+            },
+            {
+              "type": "text",
+              "content": "Now is the time for all good men to come to the aid of their party."
+            }
+          ],
+          "citation": {
+            "type": "text",
+            "content": "Charles E. Weller; <em>The Early History of the Typewriter</em>, p. 21 (1918)"
+          }
+        }
+      ],
+      "citation": {
+        "type": "text",
+        "content": "Me, on Slack, two minutes ago."
+      }
+    },
+
     {
       "_id": "649876543210",
       "type": "raw_html",
diff --git a/tests/schema-tests-05.js b/tests/schema-tests-05.js
index df511ab..1f8146f 100644
--- a/tests/schema-tests-05.js
+++ b/tests/schema-tests-05.js
@@ -396,7 +396,6 @@ describe("ANS Schema", function() {
         //tv4.addSchema(schema);
       }
 
-
     });
   });
 });
@@ -830,6 +829,15 @@ describe("Schema: ", function() {
             });
           });
 
+          describe("Quote", function() {
+            it("should not validate a quote with an invalid citation", function() {
+              validateIfFixtureExists(version, type_prefix + '/quote.json', 'quote-fixture-bad-citation', false);
+            });
+            it("should not validate a quote with no content elements", function() {
+              validateIfFixtureExists(version, type_prefix + '/quote.json', 'quote-fixture-bad-content-elements', false);
+            });
+          });
+
           describe("...all together now", function() {
             var valid_fixtures = [ "story-fixture-good", "story-fixture-references" ];
             valid_fixtures.forEach(function(fixtureName) {
@@ -838,9 +846,12 @@ describe("Schema: ", function() {
                 var document = fixtures[fixtureName];
 
                 document.content_elements.forEach(function(element) {
-                  element.type.should.equalOneOf([ "blockquote", "code", "interstitial_link", "list", "oembed", "oembed_response", "raw_html", "table", "text", "reference", "image", "video", "audio", "story", "element_group" ]);
+                  element.type.should.equalOneOf([ "blockquote", "code", "interstitial_link", "list", "oembed", "oembed_response", "raw_html", "table", "text", "reference", "image", "video", "audio", "story", "element_group", "quote" ]);
 
                   switch(element.type) {
+                  case "quote":
+                    validate(version, type_prefix + '/quote.json', element);
+                    break;
                   case "blockquote":
                     validate(version, type_prefix + '/blockquote.json', element);
                     break;
diff --git a/tests/validator.js b/tests/validator.js
index b284887..5a3279f 100644
--- a/tests/validator.js
+++ b/tests/validator.js
@@ -214,6 +214,29 @@ describe("ANS Validator", function() {
     });
 
 
+    it("should return an error if a nested content element does not validate against its own schema", function() {
+      var bad_inner_quote = {
+        "type": "story",
+        "version": "0.5.8",
+        "content_elements": [{
+          "type": "quote",
+          "content_elements": [
+            {
+              "type": "quote",
+              "content_elements": [{
+                "type": "text",
+                "content": "Citation should be a text object"
+              }],
+              "citation": "Inner citation is broken."
+            }
+          ]
+        }]
+      };
+      var errors = validator.getAllContentErrors(bad_inner_quote, version, "");
+      errors.length.should.eql(1);
+
+    });
+
   });