<commit_before>// Import packages
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const recast = require('recast');
const traverse = require('ast-traverse');
const get = require('lodash/get');
const parseCsvSync = require('csv-parse/lib/sync');
const vueCompiler = require('vue-template-compiler');
const logging = require('./logging');

// Glob path patterns
// All JS files not in node_modules
const JS_GLOB = path.resolve('./kolibri') + '/!(node_modules)/**/*.js';
// All Vue files not in node_modules
const VUE_GLOB = path.resolve('./kolibri') + '/!(node_modules)/**/*.vue';
// All of the files downloaded from Crowdin
const CSV_PATH = path.resolve('./kolibri/locale/CSV_FILES/');

// -------------------- //
// Processing Functions //
// -------------------- //

function processVueFiles(files, definitions) {
  const updatedFiles = [];

  files.forEach(filePath => {
    let fileHasChanged = false;
    const file = fs.readFileSync(filePath);

    // Parse into a SFCDescriptor (includes <template>, <script>, and <style> content)
    const vueSFC = vueCompiler.parseComponent(file.toString(), {
      preserveWhiteSpace: true,
      whitespace: 'preserve',
    });

    // Extract the needed data into an object.
    const compiledVue = {
      // Raw string of everything between <template> tags
      templateContent: get(vueSFC, 'template.content'),
      // Array of attr objects defined on <template> (eg, <template v-if="foo">)
      templateAttrs: get(vueSFC, 'template.attrs'),
      // Raw string of everything between <script> tags
      scriptContent: get(vueSFC, 'script.content'),
      // Array of attr objects defined on <script> (eg, <template type="bar/baz">)
      scriptAttrs: get(vueSFC, 'script.attrs'),
      // Array of all <style> content, attrs, etc. There may be more than 1 <style>
      // definition in a file so we just take it all here and parse it later.
      styles: get(vueSFC, 'styles', []),
    };

    // If we don't have a script, we have no work to do and should leave before breaking something.
    if (!compiledVue.scriptContent) {
      return;
    }

    // Create an AST of the <script> code.
    const scriptAST = recast.parse(compiledVue.scriptContent, {
      parser: require('recast/parsers/babylon'),
      tabWidth: 2,
      reuseWhitspace: false,
    });

    // Traverse the AST and update nodes.
    traverse(scriptAST, {
      pre: node => {
        if (is$trs(node)) {
          // node.value.properties is the object passed to $trs
          node.value.properties = node.value.properties.map(property => {
            const namespace = namespaceFromPath(filePath).replace('.vue', '');
            const key = property.key.name;

            const definition = definitions.find(o => o['Identifier'] === `${namespace}.${key}`);

            if (!definition) {
              return property;
            }

            // If we have context, assign an AST objet.
            // If we don't, then  do nothing.
            if (definition['Context'] && definition['Context'] !== '') {
              property.value = objectToAst(definition, property.value.type === 'TemplateLiteral');
              fileHasChanged = true;
            } else if (definition['Source String'] && property.type === 'ObjectProperty') {
              // We don't have context to add, but we have an ObjectProperty in the codebase
              // so we will convert it back to a regular ol' string.
              property.value =
                property.value.type === 'TemplateLiteral'
                  ? templateLiteralNode(definition)
                  : stringLiteralNode(definition);
              fileHasChanged = true;
            }
            return property;
          });
        }
      },
    });

    // No need to write the file if it hasn't changed.
    if (fileHasChanged) {
      compiledVue.scriptContent = recast.print(scriptAST, {
        reuseWhitspace: false,
        tabWidth: 2,
      }).code;
      const newFile = compileSFC(compiledVue);
      updatedFiles.push({ [filePath]: newFile });
    }
  });

  return updatedFiles;
}

function processJSFiles(files, definitions) {
  const updatedFiles = [];

  files.forEach(filePath => {
    let fileHasChanged = false;

    const file = fs.readFileSync(filePath);

    const ast = recast.parse(file, {
      parser: require('recast/parsers/babylon'),
      tabWidth: 2,
      reuseWhitspace: false,
    });

    // Traverse the AST and update nodes.
    traverse(ast, {
      pre: node => {
        // We only consider updating the node if it is a call to createTranslator()
        if (isCreateTranslator(node)) {
          // The first argument to createTranslator() is the namespace
          const namespace = node.arguments[0].value;
          // The second argument is an ObjectExpression defining the strings.
          const translations = node.arguments[1];

          // Go through all of the properties and update the nodes as needed.
          if (namespace && translations.properties) {
            node.arguments[1].properties = translations.properties.map(property => {
              const key = property.key.name;
              const definition = definitions.find(o => o['Identifier'] === `${namespace}.${key}`);

              if (!definition) {
                // We are mapping all of the properties, so be sure
                // to return the property even if we're not changing it.
                return property;
              }

              // If the definition from the CSV includes context, then we will create
              // and assign an object including the Source string and context.
              // If we don't have context to add or update, we have nothing to change here.
              if (definition['Context'] && definition['Context'] !== '') {
                property.value = objectToAst(definition, property.value.type === 'TemplateLiteral');
                fileHasChanged = true;
              } else if (definition['Source String'] && property.type === 'ObjectProperty') {
                // We don't have context to add, but we have an ObjectProperty in the codebase
                // so we will convert it back to a regular ol' string.
                property.value =
                  property.value.type === 'TemplateLiteral'
                    ? templateLiteralNode(definition)
                    : stringLiteralNode(definition);
                fileHasChanged = true;
              }
              return property;
            });
          }
        }
      },
    });

    // No need to rewrite the file if we didn't modify it.
    if (fileHasChanged) {
      const newFile = recast.print(ast, { reuseWhitspace: false, tabWidth: 2 }).code;
      updatedFiles.push({ [filePath]: newFile });
    }
  });

  return updatedFiles;
}

// ----------------- //
// Utility Functions //
// ----------------- //

// Given a vueObject, return a formatted string including
// <template> <script> <style> blocks in order.
function compileSFC(vueObject) {
  const template = compileVueTemplate(vueObject.templateContent, vueObject.templateAttrs);
  const script = compileVueScript(vueObject.scriptContent, vueObject.scriptAttrs);
  const styles = vueObject.styles
    .map(style => {
      return compileVueStyle(style.content, style.lang, style.scoped);
    })
    .join('\n\n');
  // Need to prepend the first <style> definition - but only if there is one.
  const firstStyleNewlines = styles && styles.length ? '\n\n' : '';

  return template + script + firstStyleNewlines + styles;
}

// Given the template string and any relevant attrs on the <template> definition, return the proper
// <template> block of code.
function compileVueTemplate(template, attrs) {
  if (template) {
    return `<template${attrsString(attrs)}>${template}</template>\n\n\n`;
  } else {
    return '';
  }
}

// Given the Vue file's <script> and attrs, return the <script> block as a string.
function compileVueScript(script, attrs) {
  return `<script${attrsString(attrs)}>${script}</script>\n`;
}

// Layout Vue <style> code and return the properly formatted and attributed <style> block.
function compileVueStyle(style, lang, scoped) {
  if (style || style === '') {
    const langDef = lang ? ` lang="${lang}"` : '';
    const scopedText = scoped ? ' scoped' : '';
    return `<style${langDef}${scopedText}>${style}</style>\n`;
  } else {
    return '';
  }
}

// Convert an object containing attr definitions into a string prepended with a space.
// eg, { type: 'javascript/text' } will be returned as ' type="javascript/text"'.
function attrsString(attrs) {
  return Object.keys(attrs)
    .map(key => ` ${key}="${attrs[key]}"`)
    .join('');
}

// Boolean check if a node is a call of the fn 'createTranslator()'
function isCreateTranslator(node) {
  return (
    node.type === 'CallExpression' &&
    node.callee.type === 'Identifier' &&
    node.callee.name === 'createTranslator'
  );
}

// Boolean check if a node is the definition of $trs in a Vue component.
function is$trs(node) {
  return (
    node.type === 'ObjectProperty' &&
    node.key.name === '$trs' &&
    node.value.type === 'ObjectExpression'
  );
}

// Given a definition, return an ObjectExpression that includes context & string values
// This ought to be assigned to the right-hand value of an ObjectProperty node's `value`
// where the `key` on that ObjectProperty is the `Identifier` used for the defined
// translation string.
function objectToAst(def, valueIsTemplateNode = false) {
  const sourceStringValue = valueIsTemplateNode ? templateLiteralNode(def) : stringLiteralNode(def);
  return {
    type: 'ObjectExpression',
    properties: [
      {
        type: 'ObjectProperty',
        key: {
          type: 'Identifier',
          name: 'string',
        },
        value: sourceStringValue,
      },
      {
        type: 'ObjectProperty',
        key: {
          type: 'Identifier',
          name: 'context',
        },
        value: {
          type: 'StringLiteral',
          value: def['Context'],
        },
      },
    ],
  };
}

// Given a definition, return the value node for a StringLiteral
function stringLiteralNode(def) {
  return {
    type: 'StringLiteral',
    value: def['Source String'],
  };
}

// Given a definition, return the value node for a TemplateLiteral
function templateLiteralNode(def) {
  return {
    type: 'TemplateLiteral',
    quasis: [
      {
        type: 'TemplateElement',
        tail: true,
        value: {
          raw: def['Source String'],
          cooked: def['Source String'],
        },
      },
    ],
    expressions: [],
  };
}

// Given a file's path, extract the namespace for that file's strings.
function namespaceFromPath(path) {
  const splitPath = path.split('/');
  const fileName = splitPath[splitPath.length - 1];

  // If the filename is an `index.vue` then it's parent directory name
  // indicates the namespace.
  if (fileName === 'index.vue') {
    return splitPath[splitPath.length - 2];
  } else {
    return splitPath.pop().replace('.vue', '');
  }
}

// Compile all of the defined strings & context from the CSVs that have been downloaded
// from Crowdin.
function parseCSVDefinitions(path) {
  return fs.readdirSync(path).reduce((acc, file) => {
    // Skip anything that isn't CSV - needed to avoid loading .po files.
    if (!file.endsWith('.csv')) {
      return acc;
    }
    const filePath = `${path}/${file}`;
    const csvFile = fs.readFileSync(filePath).toString();

    return (acc = [...acc, ...parseCsvSync(csvFile, { skip_empty_lines: true, columns: true })]);
  }, []);
}

// ----------------------- //
// Where The Magic Happens //
// ----------------------- //

if (process.argv.includes('run')) {
  logging.info('Transfering context...');

  const csvDefinitions = parseCSVDefinitions(CSV_PATH);

  // Load the files
  const vueFiles = glob.sync(VUE_GLOB, {});
  const jsFiles = glob.sync(JS_GLOB, {});

  // Get the updated files
  const filesToWrite = processVueFiles(vueFiles, csvDefinitions).concat(
    processJSFiles(jsFiles, csvDefinitions)
  );

  // Write the updated files
  filesToWrite.forEach(fileObj => {
    Object.keys(fileObj).forEach(path => {
      fs.writeFileSync(path, fileObj[path]);
    });
  });

  logging.info('Context transfer has completed!');
}
<commit_msg>Only modify between <script> tags in Vue files<commit_after>// Import packages
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const recast = require('recast');
const traverse = require('ast-traverse');
const get = require('lodash/get');
const parseCsvSync = require('csv-parse/lib/sync');
const vueCompiler = require('vue-template-compiler');
const logging = require('./logging');

// Regex for finding open and close <script> tags
// Will match full line unless there is a character on that line prior
// to the script tag (ie, if it is commented out).
const reScriptOpen = /^[ ]*<script[^>]*>/;
const reScriptClose = /^[ ]*<\/script>/;

// Glob path patterns
// All JS files not in node_modules
const JS_GLOB = path.resolve('./kolibri') + '/!(node_modules)/**/*.js';
// All Vue files not in node_modules
const VUE_GLOB = path.resolve('./kolibri') + '/!(node_modules)/**/*.vue';
// All of the files downloaded from Crowdin
const CSV_PATH = path.resolve('./kolibri/locale/CSV_FILES/');

// -------------------- //
// Processing Functions //
// -------------------- //

function processVueFiles(files, definitions) {
  const updatedFiles = [];

  files.forEach(filePath => {
    let fileHasChanged = false;
    const file = fs.readFileSync(filePath);

    // Parse into a SFCDescriptor (includes <template>, <script>, and <style> content)
    const vueSFC = vueCompiler.parseComponent(file.toString(), {
      preserveWhiteSpace: true,
      whitespace: 'preserve',
    });

    let scriptContent = get(vueSFC, 'script.content');

    // If we don't have a script, we have no work to do and should leave before breaking something.
    if (!scriptContent) {
      return;
    }

    // Create an AST of the <script> code.
    const scriptAST = recast.parse(scriptContent, {
      parser: require('recast/parsers/babylon'),
      tabWidth: 2,
      reuseWhitspace: false,
    });

    // Traverse the AST and update nodes.
    traverse(scriptAST, {
      pre: node => {
        if (is$trs(node)) {
          // node.value.properties is the object passed to $trs
          node.value.properties = node.value.properties.map(property => {
            const namespace = namespaceFromPath(filePath).replace('.vue', '');
            const key = property.key.name;

            const definition = definitions.find(o => o['Identifier'] === `${namespace}.${key}`);

            if (!definition) {
              return property;
            }

            // If we have context, assign an AST objet.
            // If we don't, then  do nothing.
            if (definition['Context'] && definition['Context'] !== '') {
              property.value = objectToAst(definition, property.value.type === 'TemplateLiteral');
              fileHasChanged = true;
            } else if (definition['Source String'] && property.type === 'ObjectProperty') {
              // We don't have context to add, but we have an ObjectProperty in the codebase
              // so we will convert it back to a regular ol' string.
              property.value =
                property.value.type === 'TemplateLiteral'
                  ? templateLiteralNode(definition)
                  : stringLiteralNode(definition);
              fileHasChanged = true;
            }
            return property;
          });
        }
      },
    });

    // No need to write the file if it hasn't changed.
    if (fileHasChanged) {
      const updatedScript = recast.print(scriptAST, {
        reuseWhitspace: false,
        tabWidth: 2,
      }).code;
      const newFile = injectNewScript(file.toString(), updatedScript);
      updatedFiles.push({ [filePath]: newFile });
    }
  });

  return updatedFiles;
}

function processJSFiles(files, definitions) {
  const updatedFiles = [];

  files.forEach(filePath => {
    let fileHasChanged = false;

    const file = fs.readFileSync(filePath);

    const ast = recast.parse(file, {
      parser: require('recast/parsers/babylon'),
      tabWidth: 2,
      reuseWhitspace: false,
    });

    // Traverse the AST and update nodes.
    traverse(ast, {
      pre: node => {
        // We only consider updating the node if it is a call to createTranslator()
        if (isCreateTranslator(node)) {
          // The first argument to createTranslator() is the namespace
          const namespace = node.arguments[0].value;
          // The second argument is an ObjectExpression defining the strings.
          const translations = node.arguments[1];

          // Go through all of the properties and update the nodes as needed.
          if (namespace && translations.properties) {
            node.arguments[1].properties = translations.properties.map(property => {
              const key = property.key.name;
              const definition = definitions.find(o => o['Identifier'] === `${namespace}.${key}`);

              if (!definition) {
                // We are mapping all of the properties, so be sure
                // to return the property even if we're not changing it.
                return property;
              }

              // If the definition from the CSV includes context, then we will create
              // and assign an object including the Source string and context.
              // If we don't have context to add or update, we have nothing to change here.
              if (definition['Context'] && definition['Context'] !== '') {
                property.value = objectToAst(definition, property.value.type === 'TemplateLiteral');
                fileHasChanged = true;
              } else if (definition['Source String'] && property.type === 'ObjectProperty') {
                // We don't have context to add, but we have an ObjectProperty in the codebase
                // so we will convert it back to a regular ol' string.
                property.value =
                  property.value.type === 'TemplateLiteral'
                    ? templateLiteralNode(definition)
                    : stringLiteralNode(definition);
                fileHasChanged = true;
              }
              return property;
            });
          }
        }
      },
    });

    // No need to rewrite the file if we didn't modify it.
    if (fileHasChanged) {
      const newFile = recast.print(ast, { reuseWhitspace: false, tabWidth: 2 }).code;
      updatedFiles.push({ [filePath]: newFile });
    }
  });

  return updatedFiles;
}

// ----------------- //
// Utility Functions //
// ----------------- //

// Replaces the body of <script> tags in a Vue file and replaces its content
// with that which is passed to the function.
function injectNewScript(file, content) {
  // Make both items an array of lines.
  const lines = file.split('\n');
  const contentLines = content.split('\n');

  // Will store the index positions for the open and close <script> tags.
  let open;
  let close;

  lines.forEach((line, i) => {
    // Test each line to find the opening <script...> tag
    if (reScriptOpen.test(line)) {
      open = i + 1; // Add one so the index includes the matched <script>
      return;
    }
    // Test each line to find the closing </script> tag
    if (reScriptClose.test(line)) {
      close = i - 1; // Take one away to ensure inclusion of </script>
      return;
    }
  });

  // Everything up to and including the opening <script> tag.
  const top = lines.slice(0, open);
  // Everything from (inclusively) the </script> tag to EOF.
  const bottom = lines.slice(close);

  // Combine the three arrays and append the newline Vue files need.
  return [...top, ...contentLines, ...bottom].join('\n');
}

// Boolean check if a node is a call of the fn 'createTranslator()'
function isCreateTranslator(node) {
  return (
    node.type === 'CallExpression' &&
    node.callee.type === 'Identifier' &&
    node.callee.name === 'createTranslator'
  );
}

// Boolean check if a node is the definition of $trs in a Vue component.
function is$trs(node) {
  return (
    node.type === 'ObjectProperty' &&
    node.key.name === '$trs' &&
    node.value.type === 'ObjectExpression'
  );
}

// Given a definition, return an ObjectExpression that includes context & string values
// This ought to be assigned to the right-hand value of an ObjectProperty node's `value`
// where the `key` on that ObjectProperty is the `Identifier` used for the defined
// translation string.
function objectToAst(def, valueIsTemplateNode = false) {
  const sourceStringValue = valueIsTemplateNode ? templateLiteralNode(def) : stringLiteralNode(def);
  return {
    type: 'ObjectExpression',
    properties: [
      {
        type: 'ObjectProperty',
        key: {
          type: 'Identifier',
          name: 'string',
        },
        value: sourceStringValue,
      },
      {
        type: 'ObjectProperty',
        key: {
          type: 'Identifier',
          name: 'context',
        },
        value: {
          type: 'StringLiteral',
          value: def['Context'],
        },
      },
    ],
  };
}

// Given a definition, return the value node for a StringLiteral
function stringLiteralNode(def) {
  return {
    type: 'StringLiteral',
    value: def['Source String'],
  };
}

// Given a definition, return the value node for a TemplateLiteral
function templateLiteralNode(def) {
  return {
    type: 'TemplateLiteral',
    quasis: [
      {
        type: 'TemplateElement',
        tail: true,
        value: {
          raw: def['Source String'],
          cooked: def['Source String'],
        },
      },
    ],
    expressions: [],
  };
}

// Given a file's path, extract the namespace for that file's strings.
function namespaceFromPath(path) {
  const splitPath = path.split('/');
  const fileName = splitPath[splitPath.length - 1];

  // If the filename is an `index.vue` then it's parent directory name
  // indicates the namespace.
  if (fileName === 'index.vue') {
    return splitPath[splitPath.length - 2];
  } else {
    return splitPath.pop().replace('.vue', '');
  }
}

// Compile all of the defined strings & context from the CSVs that have been downloaded
// from Crowdin.
function parseCSVDefinitions(path) {
  return fs.readdirSync(path).reduce((acc, file) => {
    // Skip anything that isn't CSV - needed to avoid loading .po files.
    if (!file.endsWith('.csv')) {
      return acc;
    }
    const filePath = `${path}/${file}`;
    const csvFile = fs.readFileSync(filePath).toString();

    return (acc = [...acc, ...parseCsvSync(csvFile, { skip_empty_lines: true, columns: true })]);
  }, []);
}

// ----------------------- //
// Where The Magic Happens //
// ----------------------- //

if (process.argv.includes('run')) {
  logging.info('Transfering context...');

  const csvDefinitions = parseCSVDefinitions(CSV_PATH);

  // Load the files
  const vueFiles = glob.sync(VUE_GLOB, {});
  const jsFiles = glob.sync(JS_GLOB, {});

  // Get the updated files
  const filesToWrite = processVueFiles(vueFiles, csvDefinitions).concat(
    processJSFiles(jsFiles, csvDefinitions)
  );

  // Write the updated files
  filesToWrite.forEach(fileObj => {
    Object.keys(fileObj).forEach(path => {
      fs.writeFileSync(path, fileObj[path]);
    });
  });

  logging.info('Context transfer has completed!');
}
<|endoftext|>


<commit_before>#!/usr/bin/env node

// assigned when options are parsed by the main function
let force

// -----------------------------------------------------------------------------

const assert = require('assert')
const flatten = require('lodash/flatten')
const getopts = require('getopts')
const limitConcurrency = require('limit-concurrency-decorator').default
const lockfile = require('proper-lockfile')
const pipe = require('promise-toolbox/pipe')
const { default: Vhd, mergeVhd } = require('vhd-lib')
const { dirname, resolve } = require('path')
const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants')
const { isValidXva } = require('@xen-orchestra/backups/isValidXva')

const asyncMap = require('../_asyncMap')
const fs = require('../_fs')

const handler = require('@xen-orchestra/fs').getHandler({ url: 'file://' })

// -----------------------------------------------------------------------------

// chain is an array of VHDs from child to parent
//
// the whole chain will be merged into parent, parent will be renamed to child
// and all the others will deleted
const mergeVhdChain = limitConcurrency(1)(async function mergeVhdChain(chain) {
  assert(chain.length >= 2)

  let child = chain[0]
  const parent = chain[chain.length - 1]
  const children = chain.slice(0, -1).reverse()

  console.warn('Unused parents of VHD', child)
  chain
    .slice(1)
    .reverse()
    .forEach(parent => {
      console.warn('  ', parent)
    })
  force && console.warn('  merging…')
  console.warn('')
  if (force) {
    // `mergeVhd` does not work with a stream, either
    // - make it accept a stream
    // - or create synthetic VHD which is not a stream
    if (children.length !== 1) {
      console.warn('TODO: implement merging multiple children')
      children.length = 1
      child = children[0]
    }

    let done, total
    const handle = setInterval(() => {
      if (done !== undefined) {
        console.log('merging %s: %s/%s', child, done, total)
      }
    }, 10e3)

    await mergeVhd(
      handler,
      parent,
      handler,
      child,
      // children.length === 1
      //   ? child
      //   : await createSyntheticStream(handler, children),
      {
        onProgress({ done: d, total: t }) {
          done = d
          total = t
        },
      }
    )

    clearInterval(handle)
  }

  await Promise.all([
    force && fs.rename(parent, child),
    asyncMap(children.slice(0, -1), child => {
      console.warn('Unused VHD', child)
      force && console.warn('  deleting…')
      console.warn('')
      return force && handler.unlink(child)
    }),
  ])
})

const listVhds = pipe([
  vmDir => vmDir + '/vdis',
  fs.readdir2,
  asyncMap(fs.readdir2),
  flatten,
  asyncMap(fs.readdir2),
  flatten,
  _ => _.filter(_ => _.endsWith('.vhd')),
])

async function handleVm(vmDir) {
  const vhds = new Set()
  const vhdParents = { __proto__: null }
  const vhdChildren = { __proto__: null }

  // remove broken VHDs
  await asyncMap(await listVhds(vmDir), async path => {
    try {
      const vhd = new Vhd(handler, path)
      await vhd.readHeaderAndFooter()
      vhds.add(path)
      if (vhd.footer.diskType === DISK_TYPE_DIFFERENCING) {
        const parent = resolve(dirname(path), vhd.header.parentUnicodeName)
        vhdParents[path] = parent
        if (parent in vhdChildren) {
          const error = new Error('this script does not support multiple VHD children')
          error.parent = parent
          error.child1 = vhdChildren[parent]
          error.child2 = path
          throw error // should we throw?
        }
        vhdChildren[parent] = path
      }
    } catch (error) {
      console.warn('Error while checking VHD', path)
      console.warn('  ', error)
      if (error != null && error.code === 'ERR_ASSERTION') {
        force && console.warn('  deleting…')
        console.warn('')
        force && (await handler.unlink(path))
      }
    }
  })

  // remove VHDs with missing ancestors
  {
    const deletions = []

    // return true if the VHD has been deleted or is missing
    const deleteIfOrphan = vhd => {
      const parent = vhdParents[vhd]
      if (parent === undefined) {
        return
      }

      // no longer needs to be checked
      delete vhdParents[vhd]

      deleteIfOrphan(parent)

      if (!vhds.has(parent)) {
        vhds.delete(vhd)

        console.warn('Error while checking VHD', vhd)
        console.warn('  missing parent', parent)
        force && console.warn('  deleting…')
        console.warn('')
        force && deletions.push(handler.unlink(vhd))
      }
    }

    // > A property that is deleted before it has been visited will not be
    // > visited later.
    // >
    // > -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Deleted_added_or_modified_properties
    for (const child in vhdParents) {
      deleteIfOrphan(child)
    }

    await Promise.all(deletions)
  }

  const [jsons, xvas, xvaSums] = await fs
    .readdir2(vmDir)
    .then(entries => [
      entries.filter(_ => _.endsWith('.json')),
      new Set(entries.filter(_ => _.endsWith('.xva'))),
      entries.filter(_ => _.endsWith('.xva.cheksum')),
    ])

  await asyncMap(xvas, async path => {
    // check is not good enough to delete the file, the best we can do is report
    // it
    if (!(await isValidXva(path))) {
      console.warn('Potential broken XVA', path)
      console.warn('')
    }
  })

  const unusedVhds = new Set(vhds)
  const unusedXvas = new Set(xvas)

  // compile the list of unused XVAs and VHDs, and remove backup metadata which
  // reference a missing XVA/VHD
  await asyncMap(jsons, async json => {
    const metadata = JSON.parse(await fs.readFile(json))
    const { mode } = metadata
    if (mode === 'full') {
      const linkedXva = resolve(vmDir, metadata.xva)

      if (xvas.has(linkedXva)) {
        unusedXvas.delete(linkedXva)
      } else {
        console.warn('Error while checking backup', json)
        console.warn('  missing file', linkedXva)
        force && console.warn('  deleting…')
        console.warn('')
        force && (await handler.unlink(json))
      }
    } else if (mode === 'delta') {
      const linkedVhds = (() => {
        const { vhds } = metadata
        return Object.keys(vhds).map(key => resolve(vmDir, vhds[key]))
      })()

      // FIXME: find better approach by keeping as much of the backup as
      // possible (existing disks) even if one disk is missing
      if (linkedVhds.every(_ => vhds.has(_))) {
        linkedVhds.forEach(_ => unusedVhds.delete(_))
      } else {
        console.warn('Error while checking backup', json)
        const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
        console.warn('  %i/%i missing VHDs', missingVhds.length, linkedVhds.length)
        missingVhds.forEach(vhd => {
          console.warn('  ', vhd)
        })
        force && console.warn('  deleting…')
        console.warn('')
        force && (await handler.unlink(json))
      }
    }
  })

  // TODO: parallelize by vm/job/vdi
  const unusedVhdsDeletion = []
  {
    // VHD chains (as list from child to ancestor) to merge indexed by last
    // ancestor
    const vhdChainsToMerge = { __proto__: null }

    const toCheck = new Set(unusedVhds)

    const getUsedChildChainOrDelete = vhd => {
      if (vhd in vhdChainsToMerge) {
        const chain = vhdChainsToMerge[vhd]
        delete vhdChainsToMerge[vhd]
        return chain
      }

      if (!unusedVhds.has(vhd)) {
        return [vhd]
      }

      // no longer needs to be checked
      toCheck.delete(vhd)

      const child = vhdChildren[vhd]
      if (child !== undefined) {
        const chain = getUsedChildChainOrDelete(child)
        if (chain !== undefined) {
          chain.push(vhd)
          return chain
        }
      }

      console.warn('Unused VHD', vhd)
      force && console.warn('  deleting…')
      console.warn('')
      force && unusedVhdsDeletion.push(handler.unlink(vhd))
    }

    toCheck.forEach(vhd => {
      vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
    })

    Object.keys(vhdChainsToMerge).forEach(key => {
      const chain = vhdChainsToMerge[key]
      if (chain !== undefined) {
        unusedVhdsDeletion.push(mergeVhdChain(chain))
      }
    })
  }

  await Promise.all([
    unusedVhdsDeletion,
    asyncMap(unusedXvas, path => {
      console.warn('Unused XVA', path)
      force && console.warn('  deleting…')
      console.warn('')
      return force && handler.unlink(path)
    }),
    asyncMap(xvaSums, path => {
      // no need to handle checksums for XVAs deleted by the script, they will be handled by `unlink()`
      if (!xvas.has(path.slice(0, -'.checksum'.length))) {
        console.warn('Unused XVA checksum', path)
        force && console.warn('  deleting…')
        console.warn('')
        return force && handler.unlink(path)
      }
    }),
  ])
}

// -----------------------------------------------------------------------------

module.exports = async function main(args) {
  const opts = getopts(args, {
    alias: {
      force: 'f',
    },
    boolean: ['force'],
    default: {
      force: false,
    },
  })

  ;({ force } = opts)
  await asyncMap(opts._, async vmDir => {
    vmDir = resolve(vmDir)

    // TODO: implement this in `xo-server`, not easy because not compatible with
    // `@xen-orchestra/fs`.
    const release = await lockfile.lock(vmDir)
    try {
      await handleVm(vmDir)
    } catch (error) {
      console.error('handleVm', vmDir, error)
    } finally {
      await release()
    }
  })
}
<commit_msg>feat(backups-cli/clean-vms): split --merge out of --force<commit_after>#!/usr/bin/env node

// assigned when options are parsed by the main function
let force, merge

// -----------------------------------------------------------------------------

const assert = require('assert')
const flatten = require('lodash/flatten')
const getopts = require('getopts')
const limitConcurrency = require('limit-concurrency-decorator').default
const lockfile = require('proper-lockfile')
const pipe = require('promise-toolbox/pipe')
const { default: Vhd, mergeVhd } = require('vhd-lib')
const { dirname, resolve } = require('path')
const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants')
const { isValidXva } = require('@xen-orchestra/backups/isValidXva')

const asyncMap = require('../_asyncMap')
const fs = require('../_fs')

const handler = require('@xen-orchestra/fs').getHandler({ url: 'file://' })

// -----------------------------------------------------------------------------

// chain is an array of VHDs from child to parent
//
// the whole chain will be merged into parent, parent will be renamed to child
// and all the others will deleted
const mergeVhdChain = limitConcurrency(1)(async function mergeVhdChain(chain) {
  assert(chain.length >= 2)

  let child = chain[0]
  const parent = chain[chain.length - 1]
  const children = chain.slice(0, -1).reverse()

  console.warn('Unused parents of VHD', child)
  chain
    .slice(1)
    .reverse()
    .forEach(parent => {
      console.warn('  ', parent)
    })
  merge && console.warn('  merging…')
  console.warn('')
  if (merge) {
    // `mergeVhd` does not work with a stream, either
    // - make it accept a stream
    // - or create synthetic VHD which is not a stream
    if (children.length !== 1) {
      console.warn('TODO: implement merging multiple children')
      children.length = 1
      child = children[0]
    }

    let done, total
    const handle = setInterval(() => {
      if (done !== undefined) {
        console.log('merging %s: %s/%s', child, done, total)
      }
    }, 10e3)

    await mergeVhd(
      handler,
      parent,
      handler,
      child,
      // children.length === 1
      //   ? child
      //   : await createSyntheticStream(handler, children),
      {
        onProgress({ done: d, total: t }) {
          done = d
          total = t
        },
      }
    )

    clearInterval(handle)
  }

  await Promise.all([
    force && fs.rename(parent, child),
    asyncMap(children.slice(0, -1), child => {
      console.warn('Unused VHD', child)
      force && console.warn('  deleting…')
      console.warn('')
      return force && handler.unlink(child)
    }),
  ])
})

const listVhds = pipe([
  vmDir => vmDir + '/vdis',
  fs.readdir2,
  asyncMap(fs.readdir2),
  flatten,
  asyncMap(fs.readdir2),
  flatten,
  _ => _.filter(_ => _.endsWith('.vhd')),
])

async function handleVm(vmDir) {
  const vhds = new Set()
  const vhdParents = { __proto__: null }
  const vhdChildren = { __proto__: null }

  // remove broken VHDs
  await asyncMap(await listVhds(vmDir), async path => {
    try {
      const vhd = new Vhd(handler, path)
      await vhd.readHeaderAndFooter()
      vhds.add(path)
      if (vhd.footer.diskType === DISK_TYPE_DIFFERENCING) {
        const parent = resolve(dirname(path), vhd.header.parentUnicodeName)
        vhdParents[path] = parent
        if (parent in vhdChildren) {
          const error = new Error('this script does not support multiple VHD children')
          error.parent = parent
          error.child1 = vhdChildren[parent]
          error.child2 = path
          throw error // should we throw?
        }
        vhdChildren[parent] = path
      }
    } catch (error) {
      console.warn('Error while checking VHD', path)
      console.warn('  ', error)
      if (error != null && error.code === 'ERR_ASSERTION') {
        force && console.warn('  deleting…')
        console.warn('')
        force && (await handler.unlink(path))
      }
    }
  })

  // remove VHDs with missing ancestors
  {
    const deletions = []

    // return true if the VHD has been deleted or is missing
    const deleteIfOrphan = vhd => {
      const parent = vhdParents[vhd]
      if (parent === undefined) {
        return
      }

      // no longer needs to be checked
      delete vhdParents[vhd]

      deleteIfOrphan(parent)

      if (!vhds.has(parent)) {
        vhds.delete(vhd)

        console.warn('Error while checking VHD', vhd)
        console.warn('  missing parent', parent)
        force && console.warn('  deleting…')
        console.warn('')
        force && deletions.push(handler.unlink(vhd))
      }
    }

    // > A property that is deleted before it has been visited will not be
    // > visited later.
    // >
    // > -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Deleted_added_or_modified_properties
    for (const child in vhdParents) {
      deleteIfOrphan(child)
    }

    await Promise.all(deletions)
  }

  const [jsons, xvas, xvaSums] = await fs
    .readdir2(vmDir)
    .then(entries => [
      entries.filter(_ => _.endsWith('.json')),
      new Set(entries.filter(_ => _.endsWith('.xva'))),
      entries.filter(_ => _.endsWith('.xva.cheksum')),
    ])

  await asyncMap(xvas, async path => {
    // check is not good enough to delete the file, the best we can do is report
    // it
    if (!(await isValidXva(path))) {
      console.warn('Potential broken XVA', path)
      console.warn('')
    }
  })

  const unusedVhds = new Set(vhds)
  const unusedXvas = new Set(xvas)

  // compile the list of unused XVAs and VHDs, and remove backup metadata which
  // reference a missing XVA/VHD
  await asyncMap(jsons, async json => {
    const metadata = JSON.parse(await fs.readFile(json))
    const { mode } = metadata
    if (mode === 'full') {
      const linkedXva = resolve(vmDir, metadata.xva)

      if (xvas.has(linkedXva)) {
        unusedXvas.delete(linkedXva)
      } else {
        console.warn('Error while checking backup', json)
        console.warn('  missing file', linkedXva)
        force && console.warn('  deleting…')
        console.warn('')
        force && (await handler.unlink(json))
      }
    } else if (mode === 'delta') {
      const linkedVhds = (() => {
        const { vhds } = metadata
        return Object.keys(vhds).map(key => resolve(vmDir, vhds[key]))
      })()

      // FIXME: find better approach by keeping as much of the backup as
      // possible (existing disks) even if one disk is missing
      if (linkedVhds.every(_ => vhds.has(_))) {
        linkedVhds.forEach(_ => unusedVhds.delete(_))
      } else {
        console.warn('Error while checking backup', json)
        const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
        console.warn('  %i/%i missing VHDs', missingVhds.length, linkedVhds.length)
        missingVhds.forEach(vhd => {
          console.warn('  ', vhd)
        })
        force && console.warn('  deleting…')
        console.warn('')
        force && (await handler.unlink(json))
      }
    }
  })

  // TODO: parallelize by vm/job/vdi
  const unusedVhdsDeletion = []
  {
    // VHD chains (as list from child to ancestor) to merge indexed by last
    // ancestor
    const vhdChainsToMerge = { __proto__: null }

    const toCheck = new Set(unusedVhds)

    const getUsedChildChainOrDelete = vhd => {
      if (vhd in vhdChainsToMerge) {
        const chain = vhdChainsToMerge[vhd]
        delete vhdChainsToMerge[vhd]
        return chain
      }

      if (!unusedVhds.has(vhd)) {
        return [vhd]
      }

      // no longer needs to be checked
      toCheck.delete(vhd)

      const child = vhdChildren[vhd]
      if (child !== undefined) {
        const chain = getUsedChildChainOrDelete(child)
        if (chain !== undefined) {
          chain.push(vhd)
          return chain
        }
      }

      console.warn('Unused VHD', vhd)
      force && console.warn('  deleting…')
      console.warn('')
      force && unusedVhdsDeletion.push(handler.unlink(vhd))
    }

    toCheck.forEach(vhd => {
      vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
    })

    Object.keys(vhdChainsToMerge).forEach(key => {
      const chain = vhdChainsToMerge[key]
      if (chain !== undefined) {
        unusedVhdsDeletion.push(mergeVhdChain(chain))
      }
    })
  }

  await Promise.all([
    unusedVhdsDeletion,
    asyncMap(unusedXvas, path => {
      console.warn('Unused XVA', path)
      force && console.warn('  deleting…')
      console.warn('')
      return force && handler.unlink(path)
    }),
    asyncMap(xvaSums, path => {
      // no need to handle checksums for XVAs deleted by the script, they will be handled by `unlink()`
      if (!xvas.has(path.slice(0, -'.checksum'.length))) {
        console.warn('Unused XVA checksum', path)
        force && console.warn('  deleting…')
        console.warn('')
        return force && handler.unlink(path)
      }
    }),
  ])
}

// -----------------------------------------------------------------------------

module.exports = async function main(args) {
  const opts = getopts(args, {
    alias: {
      force: 'f',
      merge: 'm',
    },
    boolean: ['force', 'merge'],
    default: {
      force: false,
      merge: false,
    },
  })

  ;({ force, merge } = opts)
  await asyncMap(opts._, async vmDir => {
    vmDir = resolve(vmDir)

    // TODO: implement this in `xo-server`, not easy because not compatible with
    // `@xen-orchestra/fs`.
    const release = await lockfile.lock(vmDir)
    try {
      await handleVm(vmDir)
    } catch (error) {
      console.error('handleVm', vmDir, error)
    } finally {
      await release()
    }
  })
}
<|endoftext|>


<commit_before>/*

Copyright (c) 2009 Anant Garg (anantgarg.com | inscripts.com)

This script may be used for non-commercial purposes only. For any
commercial purposes, please contact the author at
anant.garg@inscripts.com

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

*/

var windowFocus = true;
var username;
var chatHeartbeatCount = 0;
var minChatHeartbeat = 1000;
var maxChatHeartbeat = 33000;
var chatHeartbeatTime = minChatHeartbeat;
var originalTitle;
var blinkOrder = 0;

var chatboxFocus = new Array();
var newMessages = new Array();
var newMessagesWin = new Array();
var chatBoxes = new Array();

$(document).ready(function(){
	originalTitle = document.title;
	startChatSession();

	$([window, document]).blur(function(){
		windowFocus = false;
	}).focus(function(){
		windowFocus = true;
		document.title = originalTitle;
	});
});

function restructureChatBoxes() {
	align = 0;
	for (x in chatBoxes) {
		chatboxtitle = chatBoxes[x];

		if ($("#chatbox_"+chatboxtitle).css('display') != 'none') {
			if (align == 0) {
				$("#chatbox_"+chatboxtitle).css('right', '20px');
			} else {
				width = (align)*(225+7)+20;
				$("#chatbox_"+chatboxtitle).css('right', width+'px');
			}
			align++;
		}
	}
}

function chatWith(chatuser) {
	createChatBox(chatuser);
	$("#chatbox_"+chatuser+" .chatboxtextarea").focus();
}

function createChatBox(chatboxtitle,minimizeChatBox) {
	if ($("#chatbox_"+chatboxtitle).length > 0) {
		if ($("#chatbox_"+chatboxtitle).css('display') == 'none') {
			$("#chatbox_"+chatboxtitle).css('display','block');
			restructureChatBoxes();
		}
		$("#chatbox_"+chatboxtitle+" .chatboxtextarea").focus();
		return;
	}

	$("
" ).attr("id","chatbox_"+chatboxtitle)
	.addClass("chatbox")
	.html('

'+chatboxtitle+'

- X

<textarea class="chatboxtextarea" onkeydown="javascript:return checkChatBoxInputKey(event,this,\''+chatboxtitle+'\');"></textarea>
')
	.appendTo($( "body" ));

	$("#chatbox_"+chatboxtitle).css('bottom', '0px');

	chatBoxeslength = 0;

	for (x in chatBoxes) {
		if ($("#chatbox_"+chatBoxes[x]).css('display') != 'none') {
			chatBoxeslength++;
		}
	}

	if (chatBoxeslength == 0) {
		$("#chatbox_"+chatboxtitle).css('right', '20px');
	} else {
		width = (chatBoxeslength)*(225+7)+20;
		$("#chatbox_"+chatboxtitle).css('right', width+'px');
	}

	chatBoxes.push(chatboxtitle);

	if (minimizeChatBox == 1) {
		minimizedChatBoxes = new Array();

		if ($.cookie('chatbox_minimized')) {
			minimizedChatBoxes = $.cookie('chatbox_minimized').split(/\|/);
		}
		minimize = 0;
		for (j=0;j<minimizedChatBoxes.length;j++) {
			if (minimizedChatBoxes[j] == chatboxtitle) {
				minimize = 1;
			}
		}

		if (minimize == 1) {
			$('#chatbox_'+chatboxtitle+' .chatboxcontent').css('display','none');
			$('#chatbox_'+chatboxtitle+' .chatboxinput').css('display','none');
		}
	}

	chatboxFocus[chatboxtitle] = false;

	$("#chatbox_"+chatboxtitle+" .chatboxtextarea").blur(function(){
		chatboxFocus[chatboxtitle] = false;
		$("#chatbox_"+chatboxtitle+" .chatboxtextarea").removeClass('chatboxtextareaselected');
	}).focus(function(){
		chatboxFocus[chatboxtitle] = true;
		newMessages[chatboxtitle] = false;
		$('#chatbox_'+chatboxtitle+' .chatboxhead').removeClass('chatboxblink');
		$("#chatbox_"+chatboxtitle+" .chatboxtextarea").addClass('chatboxtextareaselected');
	});

	$("#chatbox_"+chatboxtitle).click(function() {
		if ($('#chatbox_'+chatboxtitle+' .chatboxcontent').css('display') != 'none') {
			$("#chatbox_"+chatboxtitle+" .chatboxtextarea").focus();
		}
	});

	$("#chatbox_"+chatboxtitle).show();
}

function chatHeartbeat(){

	var itemsfound = 0;

	if (windowFocus == false) {

		var blinkNumber = 0;
		var titleChanged = 0;
		for (x in newMessagesWin) {
			if (newMessagesWin[x] == true) {
				++blinkNumber;
				if (blinkNumber >= blinkOrder) {
					document.title = x+' says...';
					titleChanged = 1;
					break;
				}
			}
		}

		if (titleChanged == 0) {
			document.title = originalTitle;
			blinkOrder = 0;
		} else {
			++blinkOrder;
		}

	} else {
		for (x in newMessagesWin) {
			newMessagesWin[x] = false;
		}
	}

	for (x in newMessages) {
		if (newMessages[x] == true) {
			if (chatboxFocus[x] == false) {
				//FIXME: add toggle all or none policy, otherwise it looks funny
				$('#chatbox_'+x+' .chatboxhead').toggleClass('chatboxblink');
			}
		}
	}

	$.ajax({
	  url: "chat/heartbeat",
	  cache: false,
	  dataType: "json",
	  success: function(data) {

		$.each(data.items, function(i,item){
			if (item)	{ // fix strange ie bug

				chatboxtitle = item.f;

				if ($("#chatbox_"+chatboxtitle).length <= 0) {
					createChatBox(chatboxtitle);
				}
				if ($("#chatbox_"+chatboxtitle).css('display') == 'none') {
					$("#chatbox_"+chatboxtitle).css('display','block');
					restructureChatBoxes();
				}

				if (item.s == 1) {
					item.f = username;
				}

				if (item.s == 2) {
					$("#chatbox_"+chatboxtitle+" .chatboxcontent").append('
'+item.m+'
');
				} else {
					newMessages[chatboxtitle] = true;
					newMessagesWin[chatboxtitle] = true;
					$("#chatbox_"+chatboxtitle+" .chatboxcontent").append('
'+item.f+':  '+item.m+'
');
				}

				$("#chatbox_"+chatboxtitle+" .chatboxcontent").scrollTop($("#chatbox_"+chatboxtitle+" .chatboxcontent")[0].scrollHeight);
				itemsfound += 1;
			}
		});

		chatHeartbeatCount++;

		if (itemsfound > 0) {
			chatHeartbeatTime = minChatHeartbeat;
			chatHeartbeatCount = 1;
		} else if (chatHeartbeatCount >= 10) {
			chatHeartbeatTime *= 2;
			chatHeartbeatCount = 1;
			if (chatHeartbeatTime > maxChatHeartbeat) {
				chatHeartbeatTime = maxChatHeartbeat;
			}
		}

		setTimeout('chatHeartbeat();',chatHeartbeatTime);
	}});
}

function closeChatBox(chatboxtitle) {
	$('#chatbox_'+chatboxtitle).css('display','none');
	restructureChatBoxes();

	$.post("chat/close", { chatbox: chatboxtitle} , function(data){
	});

}

function toggleChatBoxGrowth(chatboxtitle) {
	if ($('#chatbox_'+chatboxtitle+' .chatboxcontent').css('display') == 'none') {

		var minimizedChatBoxes = new Array();

		if ($.cookie('chatbox_minimized')) {
			minimizedChatBoxes = $.cookie('chatbox_minimized').split(/\|/);
		}

		var newCookie = '';

		for (i=0;i<minimizedChatBoxes.length;i++) {
			if (minimizedChatBoxes[i] != chatboxtitle) {
				newCookie += chatboxtitle+'|';
			}
		}

		newCookie = newCookie.slice(0, -1)

		$.cookie('chatbox_minimized', newCookie);
		$('#chatbox_'+chatboxtitle+' .chatboxcontent').css('display','block');
		$('#chatbox_'+chatboxtitle+' .chatboxinput').css('display','block');
		$("#chatbox_"+chatboxtitle+" .chatboxcontent").scrollTop($("#chatbox_"+chatboxtitle+" .chatboxcontent")[0].scrollHeight);
	} else {

		var newCookie = chatboxtitle;

		if ($.cookie('chatbox_minimized')) {
			newCookie += '|'+$.cookie('chatbox_minimized');
		}

		$.cookie('chatbox_minimized',newCookie);
		$('#chatbox_'+chatboxtitle+' .chatboxcontent').css('display','none');
		$('#chatbox_'+chatboxtitle+' .chatboxinput').css('display','none');
	}

}

function checkChatBoxInputKey(event,chatboxtextarea,chatboxtitle) {

	if(event.keyCode == 13 && event.shiftKey == 0)  {
		message = $(chatboxtextarea).val();
		message = message.replace(/^\s+|\s+$/g,"");

		$(chatboxtextarea).val('');
		$(chatboxtextarea).focus();
		$(chatboxtextarea).css('height','44px');
		if (message != '') {
			$.post("chat/send", {to: chatboxtitle, message: message} , function(data){
				message = message.replace(/</g,"<").replace(/>/g,">").replace(/\"/g,""");
				$("#chatbox_"+chatboxtitle+" .chatboxcontent").append('
'+username+':  '+message+'
');
				$("#chatbox_"+chatboxtitle+" .chatboxcontent").scrollTop($("#chatbox_"+chatboxtitle+" .chatboxcontent")[0].scrollHeight);
			});
		}
		chatHeartbeatTime = minChatHeartbeat;
		chatHeartbeatCount = 1;

		return false;
	}

	var adjustedHeight = chatboxtextarea.clientHeight;
	var maxHeight = 94;

	if (maxHeight > adjustedHeight) {
		adjustedHeight = Math.max(chatboxtextarea.scrollHeight, adjustedHeight);
		if (maxHeight)
			adjustedHeight = Math.min(maxHeight, adjustedHeight);
		if (adjustedHeight > chatboxtextarea.clientHeight)
			$(chatboxtextarea).css('height',adjustedHeight+8 +'px');
	} else {
		$(chatboxtextarea).css('overflow','auto');
	}

}

function startChatSession(){
	$.ajax({
	  url: Routing.generate('chat_startsession'),
	  cache: false,
	  dataType: "json",
	  success: function(data) {

		username = data.username;

		$.each(data.items, function(i,item){
			if (item)	{ // fix strange ie bug

				chatboxtitle = item.f;

				if ($("#chatbox_"+chatboxtitle).length <= 0) {
					createChatBox(chatboxtitle,1);
				}

				if (item.s == 1) {
					item.f = username;
				}

				if (item.s == 2) {
					$("#chatbox_"+chatboxtitle+" .chatboxcontent").append('
'+item.m+'
');
				} else {
					$("#chatbox_"+chatboxtitle+" .chatboxcontent").append('
'+item.f+':  '+item.m+'
');
				}
			}
		});

		for (i=0;i<chatBoxes.length;i++) {
			chatboxtitle = chatBoxes[i];
			$("#chatbox_"+chatboxtitle+" .chatboxcontent").scrollTop($("#chatbox_"+chatboxtitle+" .chatboxcontent")[0].scrollHeight);
			setTimeout('$("#chatbox_"+chatboxtitle+" .chatboxcontent").scrollTop($("#chatbox_"+chatboxtitle+" .chatboxcontent")[0].scrollHeight);', 100); // yet another strange ie bug
		}

	setTimeout('chatHeartbeat();',chatHeartbeatTime);

	}});
}

/**
 * Cookie plugin
 *
 * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 */

jQuery.cookie = function(name, value, options) {
    if (typeof value != 'undefined') { // name and value given, set cookie
        options = options || {};
        if (value === null) {
            value = '';
            options.expires = -1;
        }
        var expires = '';
        if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
            var date;
            if (typeof options.expires == 'number') {
                date = new Date();
                date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
            } else {
                date = options.expires;
            }
            expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
        }
        // CAUTION: Needed to parenthesize options.path and options.domain
        // in the following expressions, otherwise they evaluate to undefined
        // in the packed version for some reason...
        var path = options.path ? '; path=' + (options.path) : '';
        var domain = options.domain ? '; domain=' + (options.domain) : '';
        var secure = options.secure ? '; secure' : '';
        document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
    } else { // only name given, get cookie
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }
};<commit_msg>Corrected chat heartbeat url routing for symfony use<commit_after>/*

Copyright (c) 2009 Anant Garg (anantgarg.com | inscripts.com)

This script may be used for non-commercial purposes only. For any
commercial purposes, please contact the author at
anant.garg@inscripts.com

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,E
XPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

*/

var windowFocus = true;
var username;
var chatHeartbeatCount = 0;
var minChatHeartbeat = 1000;
var maxChatHeartbeat = 33000;
var chatHeartbeatTime = minChatHeartbeat;
var originalTitle;
var blinkOrder = 0;

var chatboxFocus = new Array();
var newMessages = new Array();
var newMessagesWin = new Array();
var chatBoxes = new Array();

$(document).ready(function(){
	originalTitle = document.title;
	startChatSession();

	$([window, document]).blur(function(){
		windowFocus = false;
	}).focus(function(){
		windowFocus = true;
		document.title = originalTitle;
	});
});

function restructureChatBoxes() {
	align = 0;
	for (x in chatBoxes) {
		chatboxtitle = chatBoxes[x];

		if ($("#chatbox_"+chatboxtitle).css('display') != 'none') {
			if (align == 0) {
				$("#chatbox_"+chatboxtitle).css('right', '20px');
			} else {
				width = (align)*(225+7)+20;
				$("#chatbox_"+chatboxtitle).css('right', width+'px');
			}
			align++;
		}
	}
}

function chatWith(chatuser) {
	createChatBox(chatuser);
	$("#chatbox_"+chatuser+" .chatboxtextarea").focus();
}

function createChatBox(chatboxtitle,minimizeChatBox) {
	if ($("#chatbox_"+chatboxtitle).length > 0) {
		if ($("#chatbox_"+chatboxtitle).css('display') == 'none') {
			$("#chatbox_"+chatboxtitle).css('display','block');
			restructureChatBoxes();
		}
		$("#chatbox_"+chatboxtitle+" .chatboxtextarea").focus();
		return;
	}

	$("
" ).attr("id","chatbox_"+chatboxtitle)
	.addClass("chatbox")
	.html('

'+chatboxtitle+'

- X

<textarea class="chatboxtextarea" onkeydown="javascript:return checkChatBoxInputKey(event,this,\''+chatboxtitle+'\');"></textarea>
')
	.appendTo($( "body" ));

	$("#chatbox_"+chatboxtitle).css('bottom', '0px');

	chatBoxeslength = 0;

	for (x in chatBoxes) {
		if ($("#chatbox_"+chatBoxes[x]).css('display') != 'none') {
			chatBoxeslength++;
		}
	}

	if (chatBoxeslength == 0) {
		$("#chatbox_"+chatboxtitle).css('right', '20px');
	} else {
		width = (chatBoxeslength)*(225+7)+20;
		$("#chatbox_"+chatboxtitle).css('right', width+'px');
	}

	chatBoxes.push(chatboxtitle);

	if (minimizeChatBox == 1) {
		minimizedChatBoxes = new Array();

		if ($.cookie('chatbox_minimized')) {
			minimizedChatBoxes = $.cookie('chatbox_minimized').split(/\|/);
		}
		minimize = 0;
		for (j=0;j<minimizedChatBoxes.length;j++) {
			if (minimizedChatBoxes[j] == chatboxtitle) {
				minimize = 1;
			}
		}

		if (minimize == 1) {
			$('#chatbox_'+chatboxtitle+' .chatboxcontent').css('display','none');
			$('#chatbox_'+chatboxtitle+' .chatboxinput').css('display','none');
		}
	}

	chatboxFocus[chatboxtitle] = false;

	$("#chatbox_"+chatboxtitle+" .chatboxtextarea").blur(function(){
		chatboxFocus[chatboxtitle] = false;
		$("#chatbox_"+chatboxtitle+" .chatboxtextarea").removeClass('chatboxtextareaselected');
	}).focus(function(){
		chatboxFocus[chatboxtitle] = true;
		newMessages[chatboxtitle] = false;
		$('#chatbox_'+chatboxtitle+' .chatboxhead').removeClass('chatboxblink');
		$("#chatbox_"+chatboxtitle+" .chatboxtextarea").addClass('chatboxtextareaselected');
	});

	$("#chatbox_"+chatboxtitle).click(function() {
		if ($('#chatbox_'+chatboxtitle+' .chatboxcontent').css('display') != 'none') {
			$("#chatbox_"+chatboxtitle+" .chatboxtextarea").focus();
		}
	});

	$("#chatbox_"+chatboxtitle).show();
}

function chatHeartbeat(){

	var itemsfound = 0;

	if (windowFocus == false) {

		var blinkNumber = 0;
		var titleChanged = 0;
		for (x in newMessagesWin) {
			if (newMessagesWin[x] == true) {
				++blinkNumber;
				if (blinkNumber >= blinkOrder) {
					document.title = x+' says...';
					titleChanged = 1;
					break;
				}
			}
		}

		if (titleChanged == 0) {
			document.title = originalTitle;
			blinkOrder = 0;
		} else {
			++blinkOrder;
		}

	} else {
		for (x in newMessagesWin) {
			newMessagesWin[x] = false;
		}
	}

	for (x in newMessages) {
		if (newMessages[x] == true) {
			if (chatboxFocus[x] == false) {
				//FIXME: add toggle all or none policy, otherwise it looks funny
				$('#chatbox_'+x+' .chatboxhead').toggleClass('chatboxblink');
			}
		}
	}

	$.ajax({
                    url: Routing.generate('chat_heartbeat'),
	  cache: false,
	  dataType: "json",
	  success: function(data) {

		$.each(data.items, function(i,item){
			if (item)	{ // fix strange ie bug

				chatboxtitle = item.f;

				if ($("#chatbox_"+chatboxtitle).length <= 0) {
					createChatBox(chatboxtitle);
				}
				if ($("#chatbox_"+chatboxtitle).css('display') == 'none') {
					$("#chatbox_"+chatboxtitle).css('display','block');
					restructureChatBoxes();
				}

				if (item.s == 1) {
					item.f = username;
				}

				if (item.s == 2) {
					$("#chatbox_"+chatboxtitle+" .chatboxcontent").append('
'+item.m+'
');
				} else {
					newMessages[chatboxtitle] = true;
					newMessagesWin[chatboxtitle] = true;
					$("#chatbox_"+chatboxtitle+" .chatboxcontent").append('
'+item.f+':  '+item.m+'
');
				}

				$("#chatbox_"+chatboxtitle+" .chatboxcontent").scrollTop($("#chatbox_"+chatboxtitle+" .chatboxcontent")[0].scrollHeight);
				itemsfound += 1;
			}
		});

		chatHeartbeatCount++;

		if (itemsfound > 0) {
			chatHeartbeatTime = minChatHeartbeat;
			chatHeartbeatCount = 1;
		} else if (chatHeartbeatCount >= 10) {
			chatHeartbeatTime *= 2;
			chatHeartbeatCount = 1;
			if (chatHeartbeatTime > maxChatHeartbeat) {
				chatHeartbeatTime = maxChatHeartbeat;
			}
		}

		setTimeout('chatHeartbeat();',chatHeartbeatTime);
	}});
}

function closeChatBox(chatboxtitle) {
	$('#chatbox_'+chatboxtitle).css('display','none');
	restructureChatBoxes();

	$.post("chat/close", { chatbox: chatboxtitle} , function(data){
	});

}

function toggleChatBoxGrowth(chatboxtitle) {
	if ($('#chatbox_'+chatboxtitle+' .chatboxcontent').css('display') == 'none') {

		var minimizedChatBoxes = new Array();

		if ($.cookie('chatbox_minimized')) {
			minimizedChatBoxes = $.cookie('chatbox_minimized').split(/\|/);
		}

		var newCookie = '';

		for (i=0;i<minimizedChatBoxes.length;i++) {
			if (minimizedChatBoxes[i] != chatboxtitle) {
				newCookie += chatboxtitle+'|';
			}
		}

		newCookie = newCookie.slice(0, -1)

		$.cookie('chatbox_minimized', newCookie);
		$('#chatbox_'+chatboxtitle+' .chatboxcontent').css('display','block');
		$('#chatbox_'+chatboxtitle+' .chatboxinput').css('display','block');
		$("#chatbox_"+chatboxtitle+" .chatboxcontent").scrollTop($("#chatbox_"+chatboxtitle+" .chatboxcontent")[0].scrollHeight);
	} else {

		var newCookie = chatboxtitle;

		if ($.cookie('chatbox_minimized')) {
			newCookie += '|'+$.cookie('chatbox_minimized');
		}

		$.cookie('chatbox_minimized',newCookie);
		$('#chatbox_'+chatboxtitle+' .chatboxcontent').css('display','none');
		$('#chatbox_'+chatboxtitle+' .chatboxinput').css('display','none');
	}

}

function checkChatBoxInputKey(event,chatboxtextarea,chatboxtitle) {

	if(event.keyCode == 13 && event.shiftKey == 0)  {
		message = $(chatboxtextarea).val();
		message = message.replace(/^\s+|\s+$/g,"");

		$(chatboxtextarea).val('');
		$(chatboxtextarea).focus();
		$(chatboxtextarea).css('height','44px');
		if (message != '') {
			$.post("chat/send", {to: chatboxtitle, message: message} , function(data){
				message = message.replace(/</g,"<").replace(/>/g,">").replace(/\"/g,""");
				$("#chatbox_"+chatboxtitle+" .chatboxcontent").append('
'+username+':  '+message+'
');
				$("#chatbox_"+chatboxtitle+" .chatboxcontent").scrollTop($("#chatbox_"+chatboxtitle+" .chatboxcontent")[0].scrollHeight);
			});
		}
		chatHeartbeatTime = minChatHeartbeat;
		chatHeartbeatCount = 1;

		return false;
	}

	var adjustedHeight = chatboxtextarea.clientHeight;
	var maxHeight = 94;

	if (maxHeight > adjustedHeight) {
		adjustedHeight = Math.max(chatboxtextarea.scrollHeight, adjustedHeight);
		if (maxHeight)
			adjustedHeight = Math.min(maxHeight, adjustedHeight);
		if (adjustedHeight > chatboxtextarea.clientHeight)
			$(chatboxtextarea).css('height',adjustedHeight+8 +'px');
	} else {
		$(chatboxtextarea).css('overflow','auto');
	}

}

function startChatSession(){
	$.ajax({
	  url: Routing.generate('chat_startsession'),
	  cache: false,
	  dataType: "json",
	  success: function(data) {

		username = data.username;

		$.each(data.items, function(i,item){
			if (item)	{ // fix strange ie bug

				chatboxtitle = item.f;

				if ($("#chatbox_"+chatboxtitle).length <= 0) {
					createChatBox(chatboxtitle,1);
				}

				if (item.s == 1) {
					item.f = username;
				}

				if (item.s == 2) {
					$("#chatbox_"+chatboxtitle+" .chatboxcontent").append('
'+item.m+'
');
				} else {
					$("#chatbox_"+chatboxtitle+" .chatboxcontent").append('
'+item.f+':  '+item.m+'
');
				}
			}
		});

		for (i=0;i<chatBoxes.length;i++) {
			chatboxtitle = chatBoxes[i];
			$("#chatbox_"+chatboxtitle+" .chatboxcontent").scrollTop($("#chatbox_"+chatboxtitle+" .chatboxcontent")[0].scrollHeight);
			setTimeout('$("#chatbox_"+chatboxtitle+" .chatboxcontent").scrollTop($("#chatbox_"+chatboxtitle+" .chatboxcontent")[0].scrollHeight);', 100); // yet another strange ie bug
		}

	setTimeout('chatHeartbeat();',chatHeartbeatTime);

	}});
}

/**
 * Cookie plugin
 *
 * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 */

jQuery.cookie = function(name, value, options) {
    if (typeof value != 'undefined') { // name and value given, set cookie
        options = options || {};
        if (value === null) {
            value = '';
            options.expires = -1;
        }
        var expires = '';
        if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
            var date;
            if (typeof options.expires == 'number') {
                date = new Date();
                date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
            } else {
                date = options.expires;
            }
            expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
        }
        // CAUTION: Needed to parenthesize options.path and options.domain
        // in the following expressions, otherwise they evaluate to undefined
        // in the packed version for some reason...
        var path = options.path ? '; path=' + (options.path) : '';
        var domain = options.domain ? '; domain=' + (options.domain) : '';
        var secure = options.secure ? '; secure' : '';
        document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
    } else { // only name given, get cookie
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }
};<|endoftext|>


<commit_before>var GraphSvg = function () {
    var gs = Object.create (null);

    gs.plotMultiple = function (divId, title, xAxis, yAxis, graphDataArray) {
        // compute the range of the input array and use that to compute the
        // delta and a divisor that gives us less than 10 clean ticks
        var buildDomain = function (arrayOfArrays, selector, expandDelta, displaySize) {
            // a function to compute the order of magintude of a number, to use
            // for scaling
            var computeOrderOfMagnitude = function (number) {
                number = Math.max (Math.abs (number), 1.0e-6);

                // a big number, then a small number
                var order = 0;
                while (number > 10.0) {
                    ++order;
                    number /= 10.0;
                }
                while (number < 1) {
                    --order;
                    number *= 10.0;
                }
                return order;
            };

            // functions to compute the range of the input array
            var arrayFilter = function (array, filterFunc, selector) {
                var result;
                if (typeof(selector) === 'function') {
                    result = selector(array[0]);
                    for (var i = 1, count = array.length; i < count; ++i) {
                        var test = selector (array[i]);
                        result = filterFunc (result, test);
                    }
                } else {
                    result = array[0][selector];
                    for (var i = 1, count = array.length; i < count; ++i) {
                        var test = array[i][selector];
                        result = filterFunc (result, test);
                    }
                }
                return result;
            }
            var min = arrayFilter (arrayOfArrays, Math.min, function(array) { return arrayFilter (array, Math.min, selector); });
            var max = arrayFilter (arrayOfArrays, Math.max, function(array) { return arrayFilter (array, Math.max, selector); });
            var delta = max - min;
            if (delta > 0) {
                if (expandDelta) {
                    // expand the range by 1%...
                    var deltaDelta = delta * 0.01;
                    if (max != 0) {
                        max += deltaDelta;
                    }
                    if ((min != 0) && (min != 1)) {
                        min -= deltaDelta;
                    }
                    delta = max - min;
                }

                var tickOrderOfMagnitude = computeOrderOfMagnitude (delta);
                var tryScale = [1.0, 2.0, 2.5, 5.0, 10.0, 20.0, 25.0];
                var tryPrecision = [1, 1, 2, 1, 1, 1, 2];
                var tickDivisorBase = Math.pow (10, tickOrderOfMagnitude - 1);
                var tickDivisorIndex = 0;
                var tickDivisor = tickDivisorBase * tryScale[tickDivisorIndex];
                while ((delta / tickDivisor) > 9) {
                    tickDivisor = tickDivisorBase * tryScale[++tickDivisorIndex];
                }

                // now round the top and bottom to that divisor, and build the
                // domain object
                var domain = function () {
                    var domain = Object.create (null);

                    // the basics
                    domain.min = Math.floor (min / tickDivisor) * tickDivisor;
                    domain.max = Math.ceil (max / tickDivisor) * tickDivisor;
                    domain.delta = domain.max - domain.min;
                    domain.orderOfMagnitude = computeOrderOfMagnitude (domain.max);

                    // the numeric display precision
                    domain.precision = tryPrecision[tickDivisorIndex];

                    // the mapping from compute space to display space
                    domain.displaySize = displaySize;
                    domain.map = function (value) {
                        return this.displaySize * (value - this.min) / this.delta;
                    };

                    // the ticks
                    var tickCount = Math.round ((domain.max - domain.min) / tickDivisor);
                    domain.ticks = [];
                    var incr = (domain.max - domain.min) / tickCount;
                    for (var i = 0; i <= tickCount; ++i) {
                        domain.ticks.push (domain.min + (i * incr));
                    }
                    return domain;
                } ();
                return domain;
            }
            return null;
        };

        // compute the domain of the data
        var domain = {
            x: buildDomain (graphDataArray, 'x', false, 1.5),
            y: buildDomain (graphDataArray, 'y', true, 1.0),
            map: function (xy) {
                return {
                    x: this.x.map (xy.x),
                    y: this.y.map (xy.y)
                };
            }
        };

        // create the raw SVG picture for display
        var buffer = 0.15;
        var svg = '
' +
                    '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" ' +
                    'viewBox="' + ((7.0 * -buffer) / 4.0) + ', ' + (-buffer) + ', ' + (domain.x.displaySize + (3.0 * buffer)) + ', ' + (domain.y.displaySize + (2.0 * buffer)) + '" ' +
                    'preserveAspectRatio="xMidYMid meet"' +
                    '>' +
                    '<g transform="translate(0, 1), scale(1, -1)">';

        // format graph labels according to their order of magnitude and
        // desired precision
        var labelText = function (number, order, precision) {
            var     divisor = Math.pow (10, order);
            var     value = number / divisor;
            if (value != 0) {
                return value.toFixed(precision).toString () + ((order != 0) ? ("e" + order) : "");
            }
            return 0;
        };

        // draw the x ticks plus the labels
        var bottom = 0;
        var top = domain.y.displaySize;
        for (var i = 0, count = domain.x.ticks.length; i < count; ++i) {
            var ti = domain.x.ticks[i];
            var tick = domain.x.map (ti);
            svg += '<line x1="' + tick + '" y1="0" x2="' + tick + '" y2="' + top + '" stroke="#c0c0c0" stroke-width="0.005" />'
            svg += '<text  x="' + tick + '" y="0.04" font-size="0.03" font-family="Arial" dominant-baseline="middle" text-anchor="middle" fill="#808080" transform="scale(1,-1)">' + labelText (ti, domain.x.orderOfMagnitude, domain.x.precision) + '</text>';
        }

        // draw the y ticks
        var left = 0;
        var right = domain.x.displaySize;
        for (var i = 0, count = domain.y.ticks.length; i < count; ++i) {
            var ti = domain.y.ticks[i];
            var tick = domain.y.map (ti);
            svg += '<line x1="0" y1="' + tick + '" x2="' + right + '" y2="' + tick + '" stroke="#c0c0c0" stroke-width="0.005" />'
            svg += '<text  x="-0.02" y="' + -tick + '" font-size="0.03" font-family="Arial" dominant-baseline="middle" text-anchor="end" fill="#808080" transform="scale(1,-1)">' + labelText (ti, domain.y.orderOfMagnitude, domain.y.precision) + '</text>';
        }

        // draw the title
        if (this.title) {
            svg += '<text  x="' + (right / 2.0) + '" y="' + -(top + 0.075) + '" font-size="0.075" font-family="Arial" dominant-baseline="middle" text-anchor="middle" fill="#404040" transform="scale(1,-1)">' + this.title + '</text>';
        }

        // draw the x-axis label
        if (this.xAxisTitle) {
            svg += '<text  x="' + (right / 2.0) + '" y="0.1" font-size="0.05" font-family="Arial" dominant-baseline="middle" text-anchor="middle" fill="#404040" transform="scale(1,-1)">' + this.xAxisTitle + '</text>';
        }

        // draw the y-axis label
        if (this.yAxisTitle) {
            svg += '<text  x="' + (top / 2.0) + '" y="' + -(buffer + 0.025) + '" font-size="0.05" font-family="Arial" dominant-baseline="middle" text-anchor="middle" fill="#404040" transform="scale(1,-1), rotate(-90)">' + this.yAxisTitle + '</text>';
        }

        // make the plots
        var colors = ["blue", "red", "green", "orange", "purple"];
        for (var i = 0, count = graphDataArray.length; i < count; ++i) {
            svg += '<polyline fill="none" stroke="' + colors[i] + '" stroke-width="0.0075" points="';
            var graphData = graphDataArray[i];
            for (var j = 0, jcount = graphData.length; j < jcount; ++j) {
                var datum = domain.map (graphData[j]);
                svg += datum.x + ',' + datum.y + ' ';
            }
            svg += '" />';
        }

        // close the plot
        svg += "</svg>
"

        document.getElementById(divId).innerHTML += svg + "
";
    };

    gs.plotSingle = function (divId, title, xAxis, yAxis, graphData) {
        return gs.plotMultiple (divId, title, xAxis, yAxis, [graphData]);
    }

    return gs;
} ();
<commit_msg>update to remove "this." from the titles<commit_after>var GraphSvg = function () {
    var gs = Object.create (null);

    gs.plotMultiple = function (divId, title, xAxis, yAxis, graphDataArray) {
        // compute the range of the input array and use that to compute the
        // delta and a divisor that gives us less than 10 clean ticks
        var buildDomain = function (arrayOfArrays, selector, expandDelta, displaySize) {
            // a function to compute the order of magintude of a number, to use
            // for scaling
            var computeOrderOfMagnitude = function (number) {
                number = Math.max (Math.abs (number), 1.0e-6);

                // a big number, then a small number
                var order = 0;
                while (number > 10.0) {
                    ++order;
                    number /= 10.0;
                }
                while (number < 1) {
                    --order;
                    number *= 10.0;
                }
                return order;
            };

            // functions to compute the range of the input array
            var arrayFilter = function (array, filterFunc, selector) {
                var result;
                if (typeof(selector) === 'function') {
                    result = selector(array[0]);
                    for (var i = 1, count = array.length; i < count; ++i) {
                        var test = selector (array[i]);
                        result = filterFunc (result, test);
                    }
                } else {
                    result = array[0][selector];
                    for (var i = 1, count = array.length; i < count; ++i) {
                        var test = array[i][selector];
                        result = filterFunc (result, test);
                    }
                }
                return result;
            }
            var min = arrayFilter (arrayOfArrays, Math.min, function(array) { return arrayFilter (array, Math.min, selector); });
            var max = arrayFilter (arrayOfArrays, Math.max, function(array) { return arrayFilter (array, Math.max, selector); });
            var delta = max - min;
            if (delta > 0) {
                if (expandDelta) {
                    // expand the range by 1%...
                    var deltaDelta = delta * 0.01;
                    if (max != 0) {
                        max += deltaDelta;
                    }
                    if ((min != 0) && (min != 1)) {
                        min -= deltaDelta;
                    }
                    delta = max - min;
                }

                var tickOrderOfMagnitude = computeOrderOfMagnitude (delta);
                var tryScale = [1.0, 2.0, 2.5, 5.0, 10.0, 20.0, 25.0];
                var tryPrecision = [1, 1, 2, 1, 1, 1, 2];
                var tickDivisorBase = Math.pow (10, tickOrderOfMagnitude - 1);
                var tickDivisorIndex = 0;
                var tickDivisor = tickDivisorBase * tryScale[tickDivisorIndex];
                while ((delta / tickDivisor) > 9) {
                    tickDivisor = tickDivisorBase * tryScale[++tickDivisorIndex];
                }

                // now round the top and bottom to that divisor, and build the
                // domain object
                var domain = function () {
                    var domain = Object.create (null);

                    // the basics
                    domain.min = Math.floor (min / tickDivisor) * tickDivisor;
                    domain.max = Math.ceil (max / tickDivisor) * tickDivisor;
                    domain.delta = domain.max - domain.min;
                    domain.orderOfMagnitude = computeOrderOfMagnitude (domain.max);

                    // the numeric display precision
                    domain.precision = tryPrecision[tickDivisorIndex];

                    // the mapping from compute space to display space
                    domain.displaySize = displaySize;
                    domain.map = function (value) {
                        return this.displaySize * (value - this.min) / this.delta;
                    };

                    // the ticks
                    var tickCount = Math.round ((domain.max - domain.min) / tickDivisor);
                    domain.ticks = [];
                    var incr = (domain.max - domain.min) / tickCount;
                    for (var i = 0; i <= tickCount; ++i) {
                        domain.ticks.push (domain.min + (i * incr));
                    }
                    return domain;
                } ();
                return domain;
            }
            return null;
        };

        // compute the domain of the data
        var domain = {
            x: buildDomain (graphDataArray, 'x', false, 1.5),
            y: buildDomain (graphDataArray, 'y', true, 1.0),
            map: function (xy) {
                return {
                    x: this.x.map (xy.x),
                    y: this.y.map (xy.y)
                };
            }
        };

        // create the raw SVG picture for display
        var buffer = 0.15;
        var svg = '
' +
                    '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" ' +
                    'viewBox="' + ((7.0 * -buffer) / 4.0) + ', ' + (-buffer) + ', ' + (domain.x.displaySize + (3.0 * buffer)) + ', ' + (domain.y.displaySize + (2.0 * buffer)) + '" ' +
                    'preserveAspectRatio="xMidYMid meet"' +
                    '>' +
                    '<g transform="translate(0, 1), scale(1, -1)">';

        // format graph labels according to their order of magnitude and
        // desired precision
        var labelText = function (number, order, precision) {
            var     divisor = Math.pow (10, order);
            var     value = number / divisor;
            if (value != 0) {
                return value.toFixed(precision).toString () + ((order != 0) ? ("e" + order) : "");
            }
            return 0;
        };

        // draw the x ticks plus the labels
        var bottom = 0;
        var top = domain.y.displaySize;
        for (var i = 0, count = domain.x.ticks.length; i < count; ++i) {
            var ti = domain.x.ticks[i];
            var tick = domain.x.map (ti);
            svg += '<line x1="' + tick + '" y1="0" x2="' + tick + '" y2="' + top + '" stroke="#c0c0c0" stroke-width="0.005" />'
            svg += '<text  x="' + tick + '" y="0.04" font-size="0.03" font-family="Arial" dominant-baseline="middle" text-anchor="middle" fill="#808080" transform="scale(1,-1)">' + labelText (ti, domain.x.orderOfMagnitude, domain.x.precision) + '</text>';
        }

        // draw the y ticks
        var left = 0;
        var right = domain.x.displaySize;
        for (var i = 0, count = domain.y.ticks.length; i < count; ++i) {
            var ti = domain.y.ticks[i];
            var tick = domain.y.map (ti);
            svg += '<line x1="0" y1="' + tick + '" x2="' + right + '" y2="' + tick + '" stroke="#c0c0c0" stroke-width="0.005" />'
            svg += '<text  x="-0.02" y="' + -tick + '" font-size="0.03" font-family="Arial" dominant-baseline="middle" text-anchor="end" fill="#808080" transform="scale(1,-1)">' + labelText (ti, domain.y.orderOfMagnitude, domain.y.precision) + '</text>';
        }

        // draw the title
        if (title != null) {
            svg += '<text  x="' + (right / 2.0) + '" y="' + -(top + 0.075) + '" font-size="0.075" font-family="Arial" dominant-baseline="middle" text-anchor="middle" fill="#404040" transform="scale(1,-1)">' + title + '</text>';
        }

        // draw the x-axis label
        if (xAxis != null) {
            svg += '<text  x="' + (right / 2.0) + '" y="0.1" font-size="0.05" font-family="Arial" dominant-baseline="middle" text-anchor="middle" fill="#404040" transform="scale(1,-1)">' + xAxis + '</text>';
        }

        // draw the y-axis label
        if (yAxis != null) {
            svg += '<text  x="' + (top / 2.0) + '" y="' + -(buffer + 0.025) + '" font-size="0.05" font-family="Arial" dominant-baseline="middle" text-anchor="middle" fill="#404040" transform="scale(1,-1), rotate(-90)">' + yAxis + '</text>';
        }

        // make the plots
        var colors = ["blue", "red", "green", "orange", "purple"];
        for (var i = 0, count = graphDataArray.length; i < count; ++i) {
            svg += '<polyline fill="none" stroke="' + colors[i] + '" stroke-width="0.0075" points="';
            var graphData = graphDataArray[i];
            for (var j = 0, jcount = graphData.length; j < jcount; ++j) {
                var datum = domain.map (graphData[j]);
                svg += datum.x + ',' + datum.y + ' ';
            }
            svg += '" />';
        }

        // close the plot
        svg += "</svg>

"

        document.getElementById(divId).innerHTML += svg;
    };

    gs.plotSingle = function (divId, title, xAxis, yAxis, graphData) {
        return gs.plotMultiple (divId, title, xAxis, yAxis, [graphData]);
    }

    return gs;
} ();
<|endoftext|>


<commit_before>import * as delaney from './delaney';
import * as delaney2d from './delaney2d';
import * as derived from './derived';
import * as fundamental from './fundamental';
import * as properties from './properties';

const indicesExcept =
  (ds, ...idcs) => ds.indices().filter(i => !idcs.includes(i));

export const collapse = (ds, setsToSquash, connector) => {
  const remove = {};
  for (const set of setsToSquash) {
    for (const D of set)
      remove[D] = true;
  }

  const k = connector;
  const src2img = {};
  const img2src = {};
  let size = 0;

  for (const D of ds.elements()) {
    if (!remove[D]) {
      src2img[D] = ++size;
      img2src[size] = D;
    }
    else if (!remove[ds.s(k, D)])
      throw new Error(
        `must also remove ${k}-neighbor ${ds.s(k, D)} of removed element ${D}`
      );
  }

  const getS = (i, D) => {
    let E = ds.s(i, img2src[D]);
    if (i != k) {
      while (!src2img[E])
        E = ds.s(i, ds.s(k, E));
    }
    return src2img[E];
  };

  const getV = (i, D) => ds.v(i, i+1, img2src[D]) || 0;

  return delaney.buildDSymbol({ dim: ds.dim, size, getS, getV });
};

const isFundamentalTile = (ds, D) => {
  const idcs = indicesExcept(ds, ds.dim);
  const sub = derived.subsymbol(ds, idcs, D);

  if (ds.dim == 3)
    return delaney2d.curvature(sub) == 4;
  else if (ds.dim == 2)
    return properties.isLoopless(sub) && ds.v(0, 1, D) == 1;
};

const mergeTiles = (ds, seeds) => {
  const idcs = indicesExcept(ds, ds.dim - 1);

  return collapse(ds, properties.orbits(ds, idcs, seeds), ds.dim);
};

const mergeFundamentalTiles = ds => {
  const seeds = [];

  for (const [D, i] of fundamental.innerEdges(ds)) {
    if (i < ds.dim)
      continue;

    if (isFundamentalTile(ds, D) && isFundamentalTile(ds, ds.s(i, D)))
      seeds.push(D);
  }

  return mergeTiles(ds, seeds);
};

const mergeNonFundamentalTiles = ds => {
  const idcsTile = indicesExcept(ds, ds.dim);
  const idcsFacet = indicesExcept(ds, ds.dim, ds.dim - 1);

  const seeds = [];

  for (const orbit of properties.orbits(ds, idcsTile, ds.elements())) {
    if (!isFundamentalTile(ds, orbit[0]))
      continue;

    for (const E of properties.orbitReps(ds, idcsFacet, orbit)) {
      if (!isFundamentalTile(ds, ds.s(ds.dim, E))) {
        seeds.push(E);
        break;
      }
    }
  }

  return mergeTiles(ds, seeds);
};

const mergeFacets = ds => {
  const dim = delaney.dim(ds);
  const seeds = ds.elements().filter(D => delaney.m(ds, dim, dim - 1, D) == 2);
  const idcs = indicesExcept(ds, dim - 2);

  return collapse(ds, properties.orbits(ds, idcs, seeds), dim - 1);
};

const mergeEdges = ds => {
  const idcs = indicesExcept(ds, 0);
  const seeds = [];

  for (const orbit of properties.orbits(ds, idcs, ds.elements())) {
    if (orbit.every(D => delaney.m(ds, 1, 2, D) == 2))
      seeds.push(orbit[0]);
  }

  return collapse(ds, properties.orbits(ds, idcs, seeds), 1);
};

export const simplify = ds => {
  const chain = (ds, ...fns) => fns.reduce((ds, fn) => fn(ds), ds);

  ds = chain(
    ds, mergeFundamentalTiles, mergeFacets, mergeNonFundamentalTiles
  );

  let dsOld;

  do {
    dsOld = ds;
    ds = chain(
      ds, mergeFacets, mergeEdges, derived.dual, mergeFacets, derived.dual
    );
  }
  while (ds.size < dsOld.size);

  return ds;
};

if (require.main == module) {
  const test = ds => {
    console.log(`#input D-symbol`);
    console.log(`${ds}`);

    const dsx = derived.canonical(simplify(ds));
    console.log(`#output D-symbol`);
    console.log(`${dsx}`);
    console.log();
  }

  const symbols = [
    delaney.parse('<1.1:1:1,1,1:3,6>'),
    delaney.parse('<1.1:2:1 2,1 2,2:3 6,4>'),
    delaney.parse('<1.1:2 3:2,1 2,1 2,2:6,3 2,6>'),
    delaney.parse('<003.1:72 3:2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72,71 3 34 69 7 46 14 11 13 54 17 40 56 21 52 28 25 27 43 31 42 72 49 37 48 53 44 70 50 55 62 59 61 68 65 67,5 6 9 10 14 13 31 32 19 20 23 24 28 27 37 38 33 34 39 40 45 46 64 63 51 52 59 60 58 57 61 62 72 71 69 70,15 16 17 18 45 46 8 66 65 64 63 68 67 51 52 22 57 58 59 60 61 62 41 42 32 39 40 47 48 38 44 50 72 71 56 70:4 4 3 4 4 3 4 4 3 3,3 3 3 3 3 3 3 3 3 3 3 3,6 6 4 4 4 4 4 4>')
  ];

  for (const ds of symbols) {
    test(derived.barycentricSubdivision(ds));
  }
}
<commit_msg>simplify: reorder some functions<commit_after>import * as delaney from './delaney';
import * as delaney2d from './delaney2d';
import * as derived from './derived';
import * as fundamental from './fundamental';
import * as properties from './properties';

const indicesExcept =
  (ds, ...idcs) => ds.indices().filter(i => !idcs.includes(i));

export const collapse = (ds, setsToSquash, connector) => {
  const remove = {};
  for (const set of setsToSquash) {
    for (const D of set)
      remove[D] = true;
  }

  const k = connector;
  const src2img = {};
  const img2src = {};
  let size = 0;

  for (const D of ds.elements()) {
    if (!remove[D]) {
      src2img[D] = ++size;
      img2src[size] = D;
    }
    else if (!remove[ds.s(k, D)])
      throw new Error(
        `must also remove ${k}-neighbor ${ds.s(k, D)} of removed element ${D}`
      );
  }

  const getS = (i, D) => {
    let E = ds.s(i, img2src[D]);
    if (i != k) {
      while (!src2img[E])
        E = ds.s(i, ds.s(k, E));
    }
    return src2img[E];
  };

  const getV = (i, D) => ds.v(i, i+1, img2src[D]) || 0;

  return delaney.buildDSymbol({ dim: ds.dim, size, getS, getV });
};

const mergeTiles = (ds, seeds) => {
  const idcs = indicesExcept(ds, ds.dim - 1);

  return collapse(ds, properties.orbits(ds, idcs, seeds), ds.dim);
};

const mergeFacets = ds => {
  const dim = delaney.dim(ds);
  const seeds = ds.elements().filter(D => delaney.m(ds, dim, dim - 1, D) == 2);
  const idcs = indicesExcept(ds, dim - 2);

  return collapse(ds, properties.orbits(ds, idcs, seeds), dim - 1);
};

const mergeEdges = ds => {
  const idcs = indicesExcept(ds, 0);
  const seeds = [];

  for (const orbit of properties.orbits(ds, idcs, ds.elements())) {
    if (orbit.every(D => delaney.m(ds, 1, 2, D) == 2))
      seeds.push(orbit[0]);
  }

  return collapse(ds, properties.orbits(ds, idcs, seeds), 1);
};

const isFundamentalTile = (ds, D) => {
  const idcs = indicesExcept(ds, ds.dim);
  const sub = derived.subsymbol(ds, idcs, D);

  if (ds.dim == 3)
    return delaney2d.curvature(sub) == 4;
  else if (ds.dim == 2)
    return properties.isLoopless(sub) && ds.v(0, 1, D) == 1;
};

const mergeFundamentalTiles = ds => {
  const seeds = [];

  for (const [D, i] of fundamental.innerEdges(ds)) {
    if (i < ds.dim)
      continue;

    if (isFundamentalTile(ds, D) && isFundamentalTile(ds, ds.s(i, D)))
      seeds.push(D);
  }

  return mergeTiles(ds, seeds);
};

const mergeNonFundamentalTiles = ds => {
  const idcsTile = indicesExcept(ds, ds.dim);
  const idcsFacet = indicesExcept(ds, ds.dim, ds.dim - 1);

  const seeds = [];

  for (const orbit of properties.orbits(ds, idcsTile, ds.elements())) {
    if (!isFundamentalTile(ds, orbit[0]))
      continue;

    for (const E of properties.orbitReps(ds, idcsFacet, orbit)) {
      if (!isFundamentalTile(ds, ds.s(ds.dim, E))) {
        seeds.push(E);
        break;
      }
    }
  }

  return mergeTiles(ds, seeds);
};

export const simplify = ds => {
  const chain = (ds, ...fns) => fns.reduce((ds, fn) => fn(ds), ds);

  ds = chain(
    ds, mergeFundamentalTiles, mergeFacets, mergeNonFundamentalTiles
  );

  let dsOld;

  do {
    dsOld = ds;
    ds = chain(
      ds, mergeFacets, mergeEdges, derived.dual, mergeFacets, derived.dual
    );
  }
  while (ds.size < dsOld.size);

  return ds;
};

if (require.main == module) {
  const test = ds => {
    console.log(`#input D-symbol`);
    console.log(`${ds}`);

    const dsx = derived.canonical(simplify(ds));
    console.log(`#output D-symbol`);
    console.log(`${dsx}`);
    console.log();
  }

  const symbols = [
    delaney.parse('<1.1:1:1,1,1:3,6>'),
    delaney.parse('<1.1:2:1 2,1 2,2:3 6,4>'),
    delaney.parse('<1.1:2 3:2,1 2,1 2,2:6,3 2,6>'),
    delaney.parse('<003.1:72 3:2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72,71 3 34 69 7 46 14 11 13 54 17 40 56 21 52 28 25 27 43 31 42 72 49 37 48 53 44 70 50 55 62 59 61 68 65 67,5 6 9 10 14 13 31 32 19 20 23 24 28 27 37 38 33 34 39 40 45 46 64 63 51 52 59 60 58 57 61 62 72 71 69 70,15 16 17 18 45 46 8 66 65 64 63 68 67 51 52 22 57 58 59 60 61 62 41 42 32 39 40 47 48 38 44 50 72 71 56 70:4 4 3 4 4 3 4 4 3 3,3 3 3 3 3 3 3 3 3 3 3 3,6 6 4 4 4 4 4 4>')
  ];

  for (const ds of symbols) {
    test(derived.barycentricSubdivision(ds));
  }
}
<|endoftext|>