text: var parse5 = require('parse5')
  var validateTemplate = require('vue-template-validator')
  var cache = require('lru-cache')(100)
  var SourceMapGenerator = require('source-map').SourceMapGenerator
  var hash = require('hash-sum')
  var deindent = require('de-indent')
  var splitRE = /\r?\n/g
  var emptyRE = /^\s*$/
  var commentSymbols = {
    'coffee': '#',
    'coffee-jsx': '#',
    'coffee-redux': '#',
    'purs': '--',
    'ulmus': '--'
  }
  
  module.exports = function (content, filename, needMap) {
  
    var cacheKey = hash(filename + content)
    // source-map cache busting for hot-reloadded modules
    var filenameWithHash = filename + '?' + cacheKey
    var output = cache.get(cacheKey)
    if (output) return output
  
    output = {
      template: [],
      style: [],
      script: [],
      styleImports: []
    }
  
    var fragment = parse5.parseFragment(content, {
      locationInfo: true
    })
  
    fragment.childNodes.forEach(function (node) {
      var type = node.tagName
      var lang = getAttribute(node, 'lang')
      var src = getAttribute(node, 'src')
      var scoped = getAttribute(node, 'scoped') != null
      var warnings = null
      var map = null
  
      if (!output[type]) {
        return
      }
  
      // node count check
      if (
        (type === 'script' || type === 'template') &&
        output[type].length > 0
      ) {
        throw new Error(
          '[vue-loader] Only one <script> or <template> tag is ' +
          'allowed inside a Vue component.'
        )
      }
  
      // handle src imports
      if (src) {
        if (type === 'style') {
          output.styleImports.push({
            src: src,
            lang: lang,
            scoped: scoped
          })
        } else if (type === 'template') {
          output.template.push({
            src: src,
            lang: lang
          })
        } else if (type === 'script') {
          output.script.push({
            src: src,
            lang: lang
          })
        }
        return
      }
  
      // skip empty script/style tags
      if (type !== 'template' && (!node.childNodes || !node.childNodes.length)) {
        return
      }
  
      // template content is nested inside the content fragment
      if (type === 'template') {
        node = node.content
        if (!lang) {
          warnings = validateTemplate(node, content)
        }
      }
  
      // extract part
      var start = node.childNodes[0].__location.startOffset
      var end = node.childNodes[node.childNodes.length - 1].__location.endOffset
      var result
      var lineOffset
      if (type === 'script') {
        lineOffset = 0
        // preserve other parts as commenets so that linters
        // and babel can output correct line numbers in warnings
        result =
          commentScript(content.slice(0, start), lang) +
          deindent(content.slice(start, end)) +
          commentScript(content.slice(end), lang)
      } else {
        lineOffset = content.slice(0, start).split(splitRE).length - 1
        result = deindent(content.slice(start, end))
      }
  
      if (needMap) {
        // generate source map
        map = new SourceMapGenerator()
        map.setSourceContent(filenameWithHash, content)
  
        // do not add mappings for comment lines - babel's source map
        // somehow gets messed up because of it
        var isCommentLine = function (line) {
          return type === 'script' &&
            line.indexOf(getCommentSymbol(lang)) === 0
        }
  
        result.split(splitRE).forEach(function (line, index) {
          if (!emptyRE.test(line) && !isCommentLine(line)) {
            map.addMapping({
              source: filenameWithHash,
              original: {
                line: index + 1 + lineOffset,
                column: 0
              },
              generated: {
                line: index + 1,
                column: 0
              }
            })
          }
        })
        // workaround for Webpack eval-source-map bug
        // https://github.com/webpack/webpack/pull/1816
        // in case the script was piped through another loader
        // that doesn't pass down the source map properly.
        if (type === 'script' && !lang) {
          result += '\n/* generated by vue-loader */\n'
        }
      }
  
      output[type].push({
        lang: lang,
        scoped: scoped,
        content: result,
        map: map && map.toJSON(),
        warnings: warnings
      })
    })
  
    cache.set(cacheKey, output)
    return output
  }
  
  function commentScript (content, lang) {
    var symbol = getCommentSymbol(lang)
    var lines = content.split(splitRE)
    return lines.map(function (line, index) {
      // preserve EOL
      if (index === lines.length - 1 && emptyRE.test(line)) {
        return ''
      } else {
        return symbol + (emptyRE.test(line) ? '' : ' ' + line)
      }
    })
    .join('\n')
  }
  
  function getCommentSymbol (lang) {
    return commentSymbols[lang] || '//'
  }
  
  function getAttribute (node, name) {
    if (node.attrs) {
      var i = node.attrs.length
      var attr
      while (i--) {
        attr = node.attrs[i]
        if (attr.name === name) {
          return attr.value
        }
      }
    }
  }
  