<|start_filename|>book/figures.js<|end_filename|>
/* eslint-disable no-unused-vars */

function isLeftNode (node) {
  return node.parent && node.parent.children && node.parent.children[0] === node
}

function isLeaf (node) {
  return !node.children
}

// TODO: this code could be less crappy overall

function drawTree (target, root) {
  const height = 270
  const width = 720
  const marginTop = 20

  let i = 0

  const tree = d3.layout.tree().size([height, width])

  const diagonal = d3.svg.diagonal()
   .projection(function (d) { return [d.x * 2.5, d.y] })

  const svg = d3.select(target)
   .attr('width', width)
   .attr('height', height + marginTop)
    .append('g')
    .attr('transform', 'translate(0,' + marginTop + ')')

  const nodes = tree.nodes(root).reverse()
  const links = tree.links(nodes)

  // Normalize for fixed-depth.
  nodes.forEach(function (d) { d.y = d.depth * 60 })

  // Declare the nodes
  const node = svg.selectAll('g.node')
   .data(nodes, function (d) { return d.id || (d.id = ++i) })

  // Enter the nodes.
  const nodeEnter = node.enter().append('g')
    .attr('class', 'graph-node')
    .attr('transform', function (d) {
      return 'translate(' + d.x * 2.5 + ',' + d.y + ')'
    })

  nodeEnter.append('circle').attr('r', 5)

  nodeEnter.append('text')
    .attr('x', function (d) {
      if (isLeaf(d)) return 0
      return isLeftNode(d) ? -10 : 10
    })
    .attr('y', function (d) {
      return isLeaf(d) ? 20 : 0
    })
    .attr('text-anchor', function (d) {
      if (isLeftNode(d)) return 'end'
      return isLeaf(d) ? 'middle' : 'start'
    })
    .text(function (d) { return d.name })

  // Declare the link
  svg.selectAll('path.link')
    .data(links, function (d) { return d.target.id })
    .enter().insert('path', 'g')
    .attr('class', 'graph-edge')
    .attr('d', diagonal)
}

function drawScatter (target, data, xLabel, yLabel, xDomain, yDomain) {

  const margin = {top: 20, right: 30, bottom: 30, left: 40},
      width = 720 - margin.left - margin.right,
      height = 240 - margin.top - margin.bottom

  const x = d3.scale.linear().range([0, width])
  const y = d3.scale.linear().range([height, 0])

  const xAxis = d3.svg.axis().scale(x).orient('bottom')
  const yAxis = d3.svg.axis().scale(y).orient('left')

  const svg = d3.select(target)
      .attr('width', width + margin.left + margin.right)
      .attr('height', height + margin.top + margin.bottom)
    .append('g')
      .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')


  x.domain(xDomain || d3.extent(data, d => d.x)).nice()
  y.domain(yDomain || d3.extent(data, d => d.y)).nice()

  svg.append('g')
      .attr('class', 'x axis')
      .attr('transform', 'translate(0,' + height + ')')
      .call(xAxis)
    .append('text')
      .attr('class', 'label')
      .attr('x', width)
      .attr('y', -6)
      .style('text-anchor', 'end')
      .text(xLabel)

  svg.append('g')
      .attr('class', 'y axis')
      .call(yAxis)
    .append('text')
      .attr('class', 'label')
      .attr('transform', 'rotate(-90)')
      .attr('y', 6)
      .attr('dy', '.71em')
      .style('text-anchor', 'end')
      .text(yLabel)

  svg.selectAll('.dot')
      .data(data)
    .enter().append('circle')
      .attr('class', 'dot')
      .attr('r', 3.5)
      .attr('cx', d => x(d.x))
      .attr('cy', d => y(d.y))

}


<|start_filename|>book/book.css<|end_filename|>
/* COLOR SCHEME */

/*

#243340 - very dark blue, use for main text and significant lines on figures
#07c - link default

#eee - graph edge de-emphasized
*/


/* layout */

* {
  box-sizing: border-box;
}

html {
  padding: 0;
  margin: 0;
  overflow: hidden;
}

body {
  padding: 0;
  margin: 0;
}

.container {
  display: flex;
  flex-direction: row-reverse;
  align-items: stretch;
  height: 100vh;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
}

.table-of-contents {
  flex: 0 0 auto;
  font-size: 16px;
  overflow-x: hidden;
  overflow-y: auto;
  padding: 0 20px 30px 40px;
  width: 350px;
}

.chapters {
  display: flex;
  flex-wrap: wrap;
}

.chapter {
  flex: 1 1 auto;
  min-width: 320px;
}

.page {
  flex: auto;
  background: white;
  height: 100%;
  overflow: auto;
  padding: 0 40px 120px;
}

.content {
  margin: auto;
  max-width: 720px;
}

.navigation {
  margin: 60px 0;
  font-style: italic;
  text-align: right;
}

@media (max-width: 1190px) {
  .container {
    display: block;
  }
  .table-of-contents {
    width: 100%;
  }
  .page {
    height: initial;
  }
}


/* type + color */

@font-face {
  font-family: 'Whitney';
  src: url('./WhitneyHTF-Light.woff') format('woff');
  font-weight: 300;
  font-variant: normal;
}

@font-face {
  font-family: 'Whitney';
  src: url('./WhitneyHTF-Medium.woff') format('woff');
  font-weight: 400;
  font-variant: normal;
}

html {
  background: rgb(242, 242, 242);
  color: #243340;
  font-family: 'Whitney', helvetica, sans-serif;
  font-size: 18px;
  line-height: 1.6em;
}

h1, h2, h3, h4, h5, h6 {
  font-weight: lighter;
  line-height: 1.3em;
  margin: 1.5em 0 1em;
}

.table-of-contents h1 {
  margin-top: 1em;
}

.footnote {
  font-size: 14px;
  line-height: 1.2em;
}

blockquote {
  border-left: 5px solid rgba(36, 51, 64, 0.1);
  margin-left: 0;
  padding-left: 15px;
  margin-bottom: 20px;
}

a {
  color: #07c;
  text-decoration: none;
}

a:visited {
  color: #0c65a5;
}

a:hover {
  color: #30a7fc;
}

.chapter ol {
  padding: 0;
}

code, code[class*="language-"] {
  font-family: Inconsolata, Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
}

pre {
  background: rgba(242, 242, 242, 0.5);
  padding: 20px;
  overflow-x: auto;
}

figure {
  text-align: center;
  margin: 0;
}

figure img {
  max-width: 100%
}

figcaption {
  font-style: italic;
  margin: 10px 0;
}

table {
  border-collapse: separate;
  border-spacing: 0;
  max-width: 100%;
  width: 100%;
}

th {
  text-align: left;
  font-weight: bold;
}

th,
td {
  padding: 10px 10px;
  line-height: inherit;
  border-bottom-width: 1px;
  border-bottom-style: solid;
  border-bottom-color: rgba(0,0,0,.125);
}

th { vertical-align: bottom }
td { vertical-align: top }

table tr:last-child td {
  border-bottom: 0;
}

.katex-block {
  display: block;
  text-align: center;
  margin-bottom: 10px;
}

.graph-node, .dot {
  fill: #243340;
  stroke-width: 0px;
}

svg text {
  font-family: Inconsolata, Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
  font-size: 12px;
  color: #243340;
}

.graph-edge, .axis path, .axis line {
  fill: none;
  stroke: #ccc;
  stroke-width: 2px;
  /*marker-end: url(#arrowhead);*/
}

.graph-edge-emphasized {
  stroke: #243340;
  stroke-width: 4px;
}

.graph-edge-deemphasized {
  stroke: #eee;
}

#language-switcher {
  float: right;
  font-size: 16px;
  margin: 15px 0 0 20px;
  visibility: hidden;
}

[data-language-switcher-enabled] [data-language] {
  visibility: hidden;
}

#svg-refs {
  width: 0;
  height: 0;
}

sup, sub {
  vertical-align: baseline;
  position: relative;
  top: -0.4em;
}
sub {
  top: 0.4em;
}

hr {
  margin: 2em;
  border: none;
  border-bottom: 1px solid rgba(0,0,0,0.1);
}

p.crux {
  padding: 6px 18px 12px;
  background-color: rgb(222, 255, 222);
  color: rgb(70, 140, 70);
}

p.crux::before {
  content: "the crux";
  display: block;
  font-variant: small-caps;
  margin-bottom: 2px;
  font-size: 17px;
}

div.concealed {
  background: repeating-linear-gradient(
    135deg,
    rgb(242, 242, 242),
    rgb(242, 242, 242) 12px,
    rgba(0, 119, 204, 0.2) 8px,
    rgba(0, 119, 204, 0.2) 20px
  );
  position: relative;
}

div.concealed:hover {
  cursor: pointer;
  background: repeating-linear-gradient(
    135deg,
    rgb(242, 242, 242),
    rgb(242, 242, 242) 12px,
    rgba(0, 119, 204, 0.3) 8px,
    rgba(0, 119, 204, 0.3) 20px
  );
}

div.concealed * {
  visibility: hidden;
}

div.concealed::before {
  content: 'reveal';
  font-variant: small-caps;
  font-size: 17px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translateX(-50%) translateY(-50%);
  background: rgb(242, 242, 242);
  color: rgba(0, 119, 204, 0.7);
  padding: 0px 12px 0px;
  line-height: 25px;
  height: 28px;
}


<|start_filename|>prism-plugin.js<|end_filename|>
'use strict'

const prism = require('prismjs')

const pythonCode = /```python\n([\s\S]*?)```/g

const javascriptCode = /```javascript\n([\s\S]*?)```/g

const pythonGrammar = {
  'triple-quoted-string': {
    pattern: /"""[\s\S]+?"""|'''[\s\S]+?'''/,
    alias: 'string',
  },
  comment: {
    pattern: /(^|[^\\])#.*/,
    lookbehind: !0,
  },
  string: /("|')(?:\\?.)*?\1/,
  'function': {
    pattern: /((?:^|\s)def[ \t]+)[a-zA-Z_][a-zA-Z0-9_]*(?=\()/g,
    lookbehind: !0,
  },
  'class-name': {
    pattern: /(\bclass\s+)[a-z0-9_]+/i,
    lookbehind: !0,
  },
  keyword: /\b(?:as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|pass|print|raise|return|try|while|with|yield)\b/,
  'boolean': /\b(?:True|False)\b/,
  number: /\b-?(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,
  operator: /[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]|\b(?:or|and|not)\b/,
  punctuation: /[{}[\];(),.:]/,
}

const highlightPython = (match, group) => `
  <pre><code class="language-python">${prism.highlight(group, pythonGrammar)}</code></pre>
`

const highlightJavaScript = (match, group) => `
  <pre><code class="language-javascript">${prism.highlight(group, prism.languages.javascript)}</code></pre>
`

const highlightCode = files => {
  for (let path in files) {
    if (path.search('\.md$') !== -1) {
      const file = files[path]
      const replaced = file.contents
        .toString('utf8')
        .replace(pythonCode, highlightPython)
        .replace(javascriptCode, highlightJavaScript)
      file.contents = new Buffer(replaced, 'utf8')
    }
  }
}

module.exports = { highlightCode }


<|start_filename|>language-switching-plugin.js<|end_filename|>
'use strict'


const addLanguageMarkers = (files) => {
  for (let path in files) {
    if (path.search('\.html$') === -1) continue

    const file = files[path]
    const replaced =
      file.contents.toString('utf8')
        .replace(/<!-- language (\w+) -->/g, '<div data-language="$1">')
        .replace(/<!-- \/language -->/g, '</div>')

      file.contents = new Buffer(replaced, 'utf8')
  }
}

module.exports = { addLanguageMarkers }


<|start_filename|>layouts/default.html<|end_filename|>
<!DOCTYPE html>
<html>
<head>
  <title><%= title %></title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/png" href="https://bradfieldcs.com/favicon.png">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/0.0.1/prism.min.css">
  <link rel="stylesheet" href="<%= process.env.SITE_ROOT %>book.css?v=<%= Date.now() %>">

  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.10/d3.min.js"></script>
  <script src="<%= process.env.SITE_ROOT %>language-switching.js"></script>
  <script src="<%= process.env.SITE_ROOT %>figures.js"></script>
  <script src="<%= process.env.SITE_ROOT %>concealed-portions.js"></script>

</head>
<body>

<% var nextItem %>
<% for (var i = 0; i < tableOfContents.length; i++) { %>
  <% var chapter = tableOfContents[i] %>
  <% for (var j = 0; j < chapter.collection.length; j++) { %>
    <% var section = chapter.collection[j] %>
    <% if (section.path === path) { nextItem = section.next } %>
  <% } %>
<% } %>

<div class="container">

  <div class="page">
    <div class="content">
      <h1>
        <%= title %>
        <select id="language-switcher"></select>
      </h1>
      <%- contents %>

      <div class="navigation">
        <% if (nextItem) { %>
          Next:
          <a href="<%= process.env.SITE_ROOT %><%= nextItem.path %>">
            <%= nextItem.title %>
          </a>
        <% } %>
        <% if (path === '') { %>
          Next:
          <a href="<%= process.env.SITE_ROOT %>analysis/introduction">
            Introduction to Algorithm Analysis
          </a>
        <% } %>
      </div>
    </div>
  </div>

  <div class="table-of-contents">
    <h1>Practical Algorithms and&nbsp;Data&nbsp;Structures</h2>
    <a href="<%= process.env.SITE_ROOT %>">Introduction</a>

    <div class="chapters">
      <% for (var i = 0; i < tableOfContents.length; i++) { %>
        <% var chapter = tableOfContents[i] %>
        <div class="chapter">
          <h2><%= chapter.name %></h2>
          <ol>
          <% for (var j = 0; j < chapter.collection.length; j++) { %>
            <% var section = chapter.collection[j] %>
            <li>
              <% if (section.path === path) { %>
                <% nextItem = section.next %>
                <%= section.title %>
              <% } else { %>
                <a href="<%= process.env.SITE_ROOT %><%= section.path %>"><%= section.title %></a>
              <% } %>
            </li>
          <% } %>
          </ol>
        </div>
      <% } %>
    </div>

  </div>

</div><!-- .container -->

<svg id="svg-refs">
  <defs>
    <marker id="arrowhead" markerWidth="10" markerHeight="10" refx="0" refy="3" orient="auto" markerUnits="strokeWidth">
      <path d="M0,0 L0,6 L9,3 z" fill="context-stroke" />
    </marker>
  </defs>
</svg>

<!-- Google Analytics -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-71655366-1', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->

</body>
</html>


<|start_filename|>concealed-portions-plugin.js<|end_filename|>


const addConcealmentMarkers = (files) => {
  for (let path in files) {
    if (path.search('\.html$') === -1) continue

    const file = files[path]
    const replaced =
      file.contents.toString('utf8')
        .replace(/<!-- concealed -->/g, '<div class="concealed">')
        .replace(/<!-- \/concealed -->/g, '</div>')

      file.contents = new Buffer(replaced, 'utf8')
  }
}

module.exports = { addConcealmentMarkers }


<|start_filename|>build.js<|end_filename|>
'use strict'

const collections = require('metalsmith-collections')
const drafts = require('metalsmith-drafts')
const layouts = require('metalsmith-layouts')
const markdown = require('metalsmith-markdown')
const Metalsmith = require('metalsmith')
const permalinks = require('metalsmith-permalinks')

const { convertToKatex } = require('./katex-plugin')
const { addLanguageMarkers } = require('./language-switching-plugin')
const { addConcealmentMarkers } = require('./concealed-portions-plugin')
const { incorporateLiterateCode } = require('./literate-code-plugin')
const { highlightCode } = require('./prism-plugin')
const { wrapFigures } = require('./captions-plugin')

process.env.SITE_ROOT = process.env.SITE_ROOT || '/'

const BUILD_DESTINATION = process.env.BUILD_DESTINATION || '/tmp/algos-book-dev-build'

const chapters = [
  ['analysis', 'Analysis'],
  ['stacks', 'Stacks'],
  ['queues', 'Queues'],
  ['deques', 'Deques'],
  ['lists', 'Lists'],
  ['recursion', 'Recursion'],
  ['searching', 'Searching'],
  ['trees', 'Trees'],
  ['graphs', 'Graphs'],
]
const collectionConfig = {}
const nextSection = {}
const previousSection = {}
chapters.forEach(([label, name], i) => {
  collectionConfig[label] = {
    sortBy: 'position',
    metadata: { name: name },
  }
  nextSection[label] = chapters[i + 1] ? chapters[i + 1][0] : null
  previousSection[label] = chapters[i - 1] ? chapters[i - 1][0] : null
})

const INDEX_PATH = 'index.html'

// UGH! fix this ugly hack around the fact that the collections plugin
// does not link previous/next between chapters
const bridgeLinksBetweenCollections =
  (files, metalsmith) => {
    for (let path in files) {
      // ignore non-html files
      if (path.search('\.html$') === -1) continue

      const file = files[path]
      // special case: link introduction to first section
      if (path === INDEX_PATH) {
        file.next = metalsmith._metadata.collections[chapters[0][0]][0]
        continue
      }

      if (file.collection.length === 1) {
        const collection = file.collection[0]
        if (file.next === undefined) {
          const nextCollection = nextSection[collection]
          if (nextCollection) {
            file.next = metalsmith._metadata.collections[nextCollection][0]
          }
        }
        if (file.previous === undefined) {
          const previousCollection = previousSection[collection]
          if (previousCollection) {
            const collection = metalsmith._metadata.collections[previousCollection]
            file.previous = collection[collection.length - 1]
          } else {
            // special case: link first section of first chapter back to intro
            file.previous = files[INDEX_PATH]
          }
        }
      }
    }
  }

const byPosition = (a, b) => a.position - b.position

const generateTableOfContents =
  (files, metalsmith) => {
    const metadata = metalsmith.metadata()
    metadata['tableOfContents'] = chapters.map(([label, name]) => {
      return {
        name: name,
        collection: (metadata[label] || []).sort(byPosition),
      }
    })
  }

console.log(`Building to ${BUILD_DESTINATION} ..`)

const EXCLUSION_FILE_PATTERNS = [
  '\.pyc$',
  '\.py$',
  '\.DS_STORE',
]

const removeNonPublicFiles =
  files => {
    for (let path in files) {
      for (let i in EXCLUSION_FILE_PATTERNS) {
        if (path.search(EXCLUSION_FILE_PATTERNS[i]) !== -1) {
          delete files[path]
          continue
        }
      }
    }
  }

// const log = (filePath) =>
//   (files) =>
//     console.log(files[filePath].contents.toString('utf8'))

if (!process.env.TEST) {
  Metalsmith(__dirname)
  .source('book')
  .destination(BUILD_DESTINATION)
  .use(drafts())
  .use(incorporateLiterateCode)
  .use(convertToKatex)
  .use(highlightCode)
  .use(markdown({ tables: true, pedantic: true }))
  .use(addLanguageMarkers)
  .use(addConcealmentMarkers)
  // .use(log('analysis/an-anagram-detection-example.html'))
  .use(collections(collectionConfig))
  .use(bridgeLinksBetweenCollections)
  .use(wrapFigures)
  .use(removeNonPublicFiles)
  .use(permalinks())
  .use(generateTableOfContents)
  .use(layouts({ engine: 'ejs' }))
  .build(err => {
    console.log('Built')
    if (err) { throw err }
  })
}


<|start_filename|>captions-plugin.js<|end_filename|>
'use strict'

const imgTag = /<img .*? alt="([\s\S]*?)".*?>/g

const figureWithCaption = (match, group) => `
  <figure>
    ${match}
    <figcaption>${group}</figcaption>
  </figure>`

const wrapFigures = files => {
  for (let path in files) {
    if (path.search('\.html$') !== -1) {
      const file = files[path]
      const replaced = file.contents.toString('utf8').replace(imgTag, figureWithCaption)
      file.contents = new Buffer(replaced, 'utf8')
    }
  }
}

module.exports = { wrapFigures }


<|start_filename|>book/concealed-portions.js<|end_filename|>
window.addEventListener('load', () => {
  Array.from(document.getElementsByClassName('concealed')).forEach(section => {
    section.addEventListener('click', event => event.target.className = '')
  })
})


<|start_filename|>katex-plugin.js<|end_filename|>
'use strict'

const katex = require('katex')

const katexReplace = (match, group) => katex.renderToString(group)

const convertToKatex = files => {
  for (let path in files) {
    if (path.search('\.md$') !== -1) {
      const file = files[path]
      const replaced =
        file.contents.toString('utf8')
          .replace(/\n\$\$([^\$]+)\$\$\n/g, '<div class="katex-block">$$$ $1 $$$</div>')
          .replace(/\$\$([^\$]+)\$\$/g, katexReplace)
      file.contents = new Buffer(replaced, 'utf8')
    }
  }
}

module.exports = { convertToKatex }


<|start_filename|>book/language-switching.js<|end_filename|>
// TODO: litjs

const languageName = {
  javascript: 'JavaScript',
  python: 'Python',
}

const languageAttribute = 'data-language'

const unique = (values) =>
  Array.from(new Set(values).values())

const getLanguageValue = (namedNodeMap) =>
  namedNodeMap.attributes[languageAttribute].nodeValue

const querySelectionArray = (selector) =>
  Array.from(document.querySelectorAll(selector))

const getContentForAllLanguages = () =>
  querySelectionArray(`[${languageAttribute}]`)

const getContentForLanguage = (language) =>
  querySelectionArray(`[${languageAttribute}=${language}]`)

// An array of all languages defined on the page
const getUniqueLanguages = () =>
  unique(getContentForAllLanguages().map(getLanguageValue)).sort()

// Hide content other than the specified language
const switchLanguageTo = (language) => {
  getContentForAllLanguages().forEach(node => node.hidden = true)
  getContentForLanguage(language).forEach(node => node.hidden = false)
}

// When the user changes the prefered language, reflect the change
const handleSwitchLanguage = (event) => {
  const language = event.target.value
  window.localStorage.setItem('preferredLanguage', language)
  switchLanguageTo(language)
}

const renderLanguageSwitcher = () => {
  document.body.attributes['data-language-switcher-enabled'] = true
  const switcher = document.getElementById('language-switcher')
  const languages = getUniqueLanguages()
  const preferredLanguage = window.localStorage.getItem('preferredLanguage')

  languages.forEach(language => {
    const element = document.createElement('option')
    element.setAttribute('value', language)
    element.innerHTML = languageName[language] || language
    switcher.appendChild(element)
    if (language === preferredLanguage) {
      element.setAttribute('selected', true)
    }
  })

  if (languages.length > 0) {
    switcher.style.visibility = 'visible'
    getContentForAllLanguages().forEach(node => node.style.visibility = 'visible')
    switcher.addEventListener('change', handleSwitchLanguage)
    switchLanguageTo(preferredLanguage || languages[0])
  }
}

// Populate the language switcher <select> element and bind change callback
window.addEventListener('load', () => {
  renderLanguageSwitcher()
})


<|start_filename|>literate-code-plugin.js<|end_filename|>
/*
Quick and dirty literate coding style for multiple
languages.

To use, in a markdown file reference a code sample as:

  <!-- literate graphs/example.py -->

Placeholders of this form in any markdown file will
be replaced with the contents of the referenced file,
following two rules:

  1. Anything in a block comment will be concatenated
    to the markdown, for further processing; and,
  2. Anything _not_ in a block comment will be wrapped
    with markdown tags indicating that it is a section
    of code to be formatted in <pre><code></code></pre>
    tags and syntax highlighted further down the
    pipeline.

For instance, a Python literate program like so:

  """
  A **comment**
  """
  def foo(): pass

Will be converted to the following markdown in the
calling file:

  A **comment**

  ```python
  def foo(): pass
  ```
*/

'use strict'

const assert = require('assert')
const _ = require('lodash')


const placeholders = /<!-- literate (.+) -->/g


const languages = {
  py: {
    blockComment: /"""\n([\s\S]+?)\n^"""/gm,
    pragmas: /^# -\*-.+-\*-\n/g,
    syntax: 'python',
  },
  js: {
    blockComment: /\/\*\n([\s\S]+?)\n^\*\//gm,
    pragmas: /^'use strict';?\n/g,
    syntax: 'javascript',
  },
}


const removePragmas = (config, content) =>
  content.replace(config.pragmas, '')


const invert = (config, content) =>
  '```' + config.syntax + '\n' +
  content.replace(config.blockComment, '```\n$1\n```' + config.syntax) +
  '\n```'


const removeEmptyCodeBlocks = (config, content) => {
  const emptyBlocks = new RegExp('```' + config.syntax + '\n+```', 'g')
  return content.replace(emptyBlocks, '')
}


const stripNewlinesInCodeBlocks = (config, content) => {
  const multipeNewlines = new RegExp('```' + config.syntax + '\n+', 'g')
  return content
    .replace(multipeNewlines, '```' + config.syntax + '\n')
    .replace(/\n+```/g, '\n```')
}


const conversionSteps = (config) =>
  ([
    removePragmas,
    invert,
    removeEmptyCodeBlocks,
    stripNewlinesInCodeBlocks,
  ].map(f => _.partial(f, config)))


const converted = (steps, fileContent) =>
   _.flow.apply(null, steps)(fileContent)


// Return a function which when called with `replace`
// returns the markdown form of the referenced literate
// code
const replacePlaceholders = (files) =>
  (m, targetFilename) => {
    const ext = targetFilename.split('.')[1]
    const fileContent = files[targetFilename].contents.toString('utf8')
    const config = languages[ext]
    const steps = conversionSteps(config)

    return converted(steps, fileContent)
  }


// For each markdown file, find placeholders and replace
// with the converted form of the referenced files
const incorporateLiterateCode = (files) => {
  const replacer = replacePlaceholders(files)
  for (let path in files) {
    if (path.search('\.md$') === -1) continue

    const file = files[path]
    const fileContent = file.contents.toString('utf8')
    const replaced = fileContent.replace(placeholders, replacer)

    // Unfortunately, the contract is that we mutate the existing
    // file.contents
    file.contents = new Buffer(replaced, 'utf8')
  }
}

module.exports = { incorporateLiterateCode }


/* TESTS */

const test = () => {
  // Most of the literate code plugin is plumbing; the
  // important thing to test is the conversion from
  // literate form to markdown, as it uses a series
  // of fiddly rexexp

  const convertPython = _.partial(converted, conversionSteps(languages.py))
  const convertJS = _.partial(converted, conversionSteps(languages.js))

  assert.equal(convertPython(`
"""
A comment with _markdown_
"""
def foo_bar_baz():
    pass
`),
    `
A comment with _markdown_
\`\`\`python
def foo_bar_baz():
    pass
\`\`\``)

  assert.equal(convertJS(`
/*
A comment with _markdown_
*/
const foo = () => {}
`),
    `
A comment with _markdown_
\`\`\`javascript
const foo = () => {}
\`\`\``)

}

if (process.env.TEST) test()
