<commit_before>import { ipcRenderer } from 'electron';
import path from 'path';
import { autorun, computed, observable } from 'mobx';
import { loadModule } from 'cld3-asm';
import { debounce } from 'lodash';

import RecipeWebview from './lib/RecipeWebview';

import spellchecker, { switchDict, disable as disableSpellchecker, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker';
import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode';
import contextMenu from './contextMenu';
import './notifications';

import { DEFAULT_APP_SETTINGS } from '../config';
import { isDevMode } from '../environment';

const debug = require('debug')('Franz:Plugin');

class RecipeController {
  @observable settings = {
    overrideSpellcheckerLanguage: false,
    app: DEFAULT_APP_SETTINGS,
    service: {
      isDarkModeEnabled: false,
      spellcheckerLanguage: '',
    },
  };

  spellcheckProvider = null;

  ipcEvents = {
    'initialize-recipe': 'loadRecipeModule',
    'settings-update': 'updateAppSettings',
    'service-settings-update': 'updateServiceSettings',
    'get-service-id': 'serviceIdEcho',
  }

  constructor() {
    this.initialize();
  }

  @computed get spellcheckerLanguage() {
    return this.settings.service.spellcheckerLanguage || this.settings.app.spellcheckerLanguage;
  }

  cldIdentifier = null;

  async initialize() {
    Object.keys(this.ipcEvents).forEach((channel) => {
      ipcRenderer.on(channel, (...args) => {
        debug('Received IPC event for channel', channel, 'with', ...args);
        this[this.ipcEvents[channel]](...args);
      });
    });

    debug('Send "hello" to host');
    setTimeout(() => ipcRenderer.sendToHost('hello'), 100);

    this.spellcheckingProvider = await spellchecker();
    contextMenu(
      this.spellcheckingProvider,
      () => this.settings.app.enableSpellchecking,
      () => this.settings.app.spellcheckerLanguage,
      () => this.spellcheckerLanguage,
    );

    autorun(() => this.update());
  }

  loadRecipeModule(event, config, recipe) {
    debug('loadRecipeModule');
    const modulePath = path.join(recipe.path, 'webview.js');
    debug('module path', modulePath);
    // Delete module from cache
    delete require.cache[require.resolve(modulePath)];
    try {
      // eslint-disable-next-line
      require(modulePath)(new RecipeWebview(), {...config, recipe,});
      debug('Initialize Recipe', config, recipe);

      this.settings.service = Object.assign(config, { recipe });
    } catch (err) {
      console.error('Recipe initialization failed', err);
    }
  }

  update() {
    debug('enableSpellchecking', this.settings.app.enableSpellchecking);
    debug('isDarkModeEnabled', this.settings.service.isDarkModeEnabled);
    debug('System spellcheckerLanguage', this.settings.app.spellcheckerLanguage);
    debug('Service spellcheckerLanguage', this.settings.service.spellcheckerLanguage);

    if (this.settings.app.enableSpellchecking) {
      debug('Setting spellchecker language to', this.spellcheckerLanguage);
      let { spellcheckerLanguage } = this;
      if (spellcheckerLanguage === 'automatic') {
        this.automaticLanguageDetection();
        debug('Found `automatic` locale, falling back to user locale until detected', this.settings.app.locale);
        spellcheckerLanguage = this.settings.app.locale;
      } else if (this.cldIdentifier) {
        this.cldIdentifier.destroy();
      }
      switchDict(spellcheckerLanguage);
    } else {
      debug('Disable spellchecker');
      disableSpellchecker();

      if (this.cldIdentifier) {
        this.cldIdentifier.destroy();
      }
    }

    if (this.settings.service.isDarkModeEnabled) {
      debug('Enable dark mode');
      injectDarkModeStyle(this.settings.service.recipe.path);
    } else if (isDarkModeStyleInjected()) {
      debug('Remove dark mode');
      removeDarkModeStyle();
    }
  }

  updateAppSettings(event, data) {
    this.settings.app = Object.assign(this.settings.app, data);
  }

  updateServiceSettings(event, data) {
    this.settings.service = Object.assign(this.settings.service, data);
  }

  serviceIdEcho(event) {
    debug('Received a service echo ping');
    event.sender.send('service-id', this.settings.service.id);
  }

  async automaticLanguageDetection() {
    const cldFactory = await loadModule();
    this.cldIdentifier = cldFactory.create(0, 1000);

    window.addEventListener('keyup', debounce((e) => {
      const element = e.target;

      if (!element) return;

      let value = '';
      if (element.isContentEditable) {
        value = element.textContent;
      } else if (element.value) {
        value = element.value;
      }

      // Force a minimum length to get better detection results
      if (value.length < 30) return;

      debug('Detecting language for', value);
      const findResult = this.cldIdentifier.findLanguage(value);

      debug('Language detection result', findResult);

      if (findResult.is_reliable) {
        const spellcheckerLocale = getSpellcheckerLocaleByFuzzyIdentifier(findResult.language);
        debug('Language detected reliably, setting spellchecker language to', spellcheckerLocale);
        if (spellcheckerLocale) {
          switchDict(spellcheckerLocale);
        }
      }
    }, 225));
  }
}

/* eslint-disable no-new */
new RecipeController();
/* eslint-enable no-new */

// Patching window.open
const originalWindowOpen = window.open;

window.open = (url, frameName, features) => {
  debug('window.open', url, frameName, features);
  // We need to differentiate if the link should be opened in a popup or in the systems default browser
  if (!frameName && !features && typeof features !== 'string') {
    return ipcRenderer.sendToHost('new-window', url);
  }

  return originalWindowOpen(url, frameName, features);
};
<commit_msg>Add `window.log` for easier logging in event mode<commit_after>import { ipcRenderer } from 'electron';
import path from 'path';
import { autorun, computed, observable } from 'mobx';
import { loadModule } from 'cld3-asm';
import { debounce } from 'lodash';

import RecipeWebview from './lib/RecipeWebview';

import spellchecker, { switchDict, disable as disableSpellchecker, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker';
import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode';
import contextMenu from './contextMenu';
import './notifications';

import { DEFAULT_APP_SETTINGS } from '../config';
import { isDevMode } from '../environment';

const debug = require('debug')('Franz:Plugin');

class RecipeController {
  @observable settings = {
    overrideSpellcheckerLanguage: false,
    app: DEFAULT_APP_SETTINGS,
    service: {
      isDarkModeEnabled: false,
      spellcheckerLanguage: '',
    },
  };

  spellcheckProvider = null;

  ipcEvents = {
    'initialize-recipe': 'loadRecipeModule',
    'settings-update': 'updateAppSettings',
    'service-settings-update': 'updateServiceSettings',
    'get-service-id': 'serviceIdEcho',
  }

  constructor() {
    this.initialize();
  }

  @computed get spellcheckerLanguage() {
    return this.settings.service.spellcheckerLanguage || this.settings.app.spellcheckerLanguage;
  }

  cldIdentifier = null;

  async initialize() {
    Object.keys(this.ipcEvents).forEach((channel) => {
      ipcRenderer.on(channel, (...args) => {
        debug('Received IPC event for channel', channel, 'with', ...args);
        this[this.ipcEvents[channel]](...args);
      });
    });

    debug('Send "hello" to host');
    setTimeout(() => ipcRenderer.sendToHost('hello'), 100);

    this.spellcheckingProvider = await spellchecker();
    contextMenu(
      this.spellcheckingProvider,
      () => this.settings.app.enableSpellchecking,
      () => this.settings.app.spellcheckerLanguage,
      () => this.spellcheckerLanguage,
    );

    autorun(() => this.update());
  }

  loadRecipeModule(event, config, recipe) {
    debug('loadRecipeModule');
    const modulePath = path.join(recipe.path, 'webview.js');
    debug('module path', modulePath);
    // Delete module from cache
    delete require.cache[require.resolve(modulePath)];
    try {
      // eslint-disable-next-line
      require(modulePath)(new RecipeWebview(), {...config, recipe,});
      debug('Initialize Recipe', config, recipe);

      this.settings.service = Object.assign(config, { recipe });
    } catch (err) {
      console.error('Recipe initialization failed', err);
    }
  }

  update() {
    debug('enableSpellchecking', this.settings.app.enableSpellchecking);
    debug('isDarkModeEnabled', this.settings.service.isDarkModeEnabled);
    debug('System spellcheckerLanguage', this.settings.app.spellcheckerLanguage);
    debug('Service spellcheckerLanguage', this.settings.service.spellcheckerLanguage);

    if (this.settings.app.enableSpellchecking) {
      debug('Setting spellchecker language to', this.spellcheckerLanguage);
      let { spellcheckerLanguage } = this;
      if (spellcheckerLanguage === 'automatic') {
        this.automaticLanguageDetection();
        debug('Found `automatic` locale, falling back to user locale until detected', this.settings.app.locale);
        spellcheckerLanguage = this.settings.app.locale;
      } else if (this.cldIdentifier) {
        this.cldIdentifier.destroy();
      }
      switchDict(spellcheckerLanguage);
    } else {
      debug('Disable spellchecker');
      disableSpellchecker();

      if (this.cldIdentifier) {
        this.cldIdentifier.destroy();
      }
    }

    if (this.settings.service.isDarkModeEnabled) {
      debug('Enable dark mode');
      injectDarkModeStyle(this.settings.service.recipe.path);
    } else if (isDarkModeStyleInjected()) {
      debug('Remove dark mode');
      removeDarkModeStyle();
    }
  }

  updateAppSettings(event, data) {
    this.settings.app = Object.assign(this.settings.app, data);
  }

  updateServiceSettings(event, data) {
    this.settings.service = Object.assign(this.settings.service, data);
  }

  serviceIdEcho(event) {
    debug('Received a service echo ping');
    event.sender.send('service-id', this.settings.service.id);
  }

  async automaticLanguageDetection() {
    const cldFactory = await loadModule();
    this.cldIdentifier = cldFactory.create(0, 1000);

    window.addEventListener('keyup', debounce((e) => {
      const element = e.target;

      if (!element) return;

      let value = '';
      if (element.isContentEditable) {
        value = element.textContent;
      } else if (element.value) {
        value = element.value;
      }

      // Force a minimum length to get better detection results
      if (value.length < 30) return;

      debug('Detecting language for', value);
      const findResult = this.cldIdentifier.findLanguage(value);

      debug('Language detection result', findResult);

      if (findResult.is_reliable) {
        const spellcheckerLocale = getSpellcheckerLocaleByFuzzyIdentifier(findResult.language);
        debug('Language detected reliably, setting spellchecker language to', spellcheckerLocale);
        if (spellcheckerLocale) {
          switchDict(spellcheckerLocale);
        }
      }
    }, 225));
  }
}

/* eslint-disable no-new */
new RecipeController();
/* eslint-enable no-new */

// Patching window.open
const originalWindowOpen = window.open;

window.open = (url, frameName, features) => {
  debug('window.open', url, frameName, features);
  // We need to differentiate if the link should be opened in a popup or in the systems default browser
  if (!frameName && !features && typeof features !== 'string') {
    return ipcRenderer.sendToHost('new-window', url);
  }

  return originalWindowOpen(url, frameName, features);
};

if (isDevMode) {
  window.log = console.log;
}
<|endoftext|>


<commit_before>import "./lib/lazyload";

window.listView = {
    initialized: false,
    imagePrefix: null,
    defaultImage: null,
    listScrollingContainer: null,
    imageHeight: null,
    imageWidth: null,
    addPlaces: (places) => {
        if(!places){
            return;
        }

        function init() {
            console.log('Initializing list view !!!');
            // Crop image to 16:9 aspect ratio
            window.listView.imageWidth = Math.floor(window.innerWidth);
            window.listView.imageHeight = Math.floor(window.innerWidth / 16 * 9);

            const cloudImg = window.app.settings.cloudImg;
            window.listView.imagePrefix = `${cloudImg.domain}${cloudImg.operations.crop}/${window.listView.imageWidth}x${window.listView.imageHeight}/`;
            window.listView.defaultImage = `${cloudImg.domain}${cloudImg.operations.cdn}/https://pluginserver.buildfire.com/styles/media/holder-16x9.png`;

            const listContainer = document.getElementById("listView");

            window.listView.listScrollingContainer = document.createElement('div');
            window.listView.listScrollingContainer.className = 'list-scrolling-container';
            window.listView.listScrollingContainer.id = 'list--container';
            listContainer.appendChild(window.listView.listScrollingContainer);
            window.listView.initialized = true;
        }
        if (!window.listView.initialized) {
           init();
        }

        let sortPlaces = [];
        sortPlaces = places.sort(window.PlacesSort[window.app.state.sortBy]);
        window.listView.sorting(sortPlaces);
        window.lazyload();
    },
    sorting: (places) => {
        if (typeof (window.listView.listScrollingContainer) != undefined && window.listView.listScrollingContainer != null) {
            window.listView.listScrollingContainer.querySelectorAll('*').forEach(node => node.remove());
        }
            places.forEach((place, index) => {

                if (!place.address || !place.address.lat || !place.address.lng) {
                    return;
                }

                const listItem = document.createElement('div');
                listItem.setAttribute('style', `${window.listView.imageHeight}px !important`);
                listItem.id = (place.id) ? `id_${place.id}` : '';
                listItem.className = 'list-item';

                listItem.addEventListener('click', e => {
                    e.preventDefault();
                    window.app.state.selectedPlace.unshift(place);
                    window.router.navigate(window.app.settings.viewStates.detail);
                });

                //Add Image
                const listImage = place.image ? place.image : window.listView.defaultImage;
                const image = document.createElement('img');

                image.setAttribute('data-src', window.listView.imagePrefix + listImage);
                image.setAttribute('width', window.listView.imageWidth);
                image.setAttribute('height', window.listView.imageHeight);
                image.setAttribute('style', `${window.listView.imageHeight}px !important`);
                image.className = 'lazyload';

                const infoContainer = document.createElement('div');
                infoContainer.className = 'list-info-container';

                const title = document.createElement('div');
                title.className = 'list-title';
                title.innerHTML = place.title;
                infoContainer.appendChild(title);

                const subtitle = document.createElement('div');
                let subtitleText = (place.subtitle && place.subtitle.length)
                    ? place.subtitle : '';

                subtitle.className = 'list-description';
                subtitle.innerHTML = subtitleText;
                infoContainer.appendChild(subtitle);

                const viewBtn = document.createElement('img');
                viewBtn.className = 'list-view-btn';
                viewBtn.src = 'images/right-arrow.png';
                infoContainer.appendChild(viewBtn);

                const address = document.createElement('div');
                address.innerHTML = place.address;

                const distance = document.createElement('div');
                distance.setAttribute('id', `distance-${place.id}`);
                distance.innerHTML = (place.distance) ? place.distance : '...';
                distance.className = 'list-distance';
                infoContainer.appendChild(distance);

                listItem.appendChild(image);
                listItem.appendChild(infoContainer);
                if (typeof (window.listView.listScrollingContainer) != undefined && window.listView.listScrollingContainer != null) {
                    window.listView.listScrollingContainer.appendChild(listItem);
                }
            });
    },
    initList: (places) => {
        //Add filter control
        let filterDiv = document.getElementById('filter');
        new window.FilterControl(filterDiv);
        window.listView.addPlaces(places);
    },
    updateList: (newPlaces) => {
        console.log('called updateList()');
        window.listView.addPlaces(newPlaces);
    },
    filter(placesToHide, placesToShow) {
        //Hide filtered places
        placesToHide.forEach((place) => {
            let divToHide = document.getElementById(`id_${place.id}`);
            if(divToHide)
                divToHide.setAttribute('style', 'display:none !important');
        });

        //Show places that have been hidden
        placesToShow.forEach((place) => {
            let divToShow = document.getElementById(`id_${place.id}`);
            if(divToShow)
                divToShow.setAttribute('style', 'display:block !important');
        });
    },
    updateDistances: (places) => {
        places.forEach((place, index) => {
            let distanceElement = document.getElementById(`distance-${place.id}`);

            if(distanceElement)
                distanceElement.innerHTML = place.distance;
        });
    }
};<commit_msg>Replaced ... with blank on locations distance (#73)<commit_after>import "./lib/lazyload";

window.listView = {
    initialized: false,
    imagePrefix: null,
    defaultImage: null,
    listScrollingContainer: null,
    imageHeight: null,
    imageWidth: null,
    addPlaces: (places) => {
        if(!places){
            return;
        }

        function init() {
            console.log('Initializing list view !!!');
            // Crop image to 16:9 aspect ratio
            window.listView.imageWidth = Math.floor(window.innerWidth);
            window.listView.imageHeight = Math.floor(window.innerWidth / 16 * 9);

            const cloudImg = window.app.settings.cloudImg;
            window.listView.imagePrefix = `${cloudImg.domain}${cloudImg.operations.crop}/${window.listView.imageWidth}x${window.listView.imageHeight}/`;
            window.listView.defaultImage = `${cloudImg.domain}${cloudImg.operations.cdn}/https://pluginserver.buildfire.com/styles/media/holder-16x9.png`;

            const listContainer = document.getElementById("listView");

            window.listView.listScrollingContainer = document.createElement('div');
            window.listView.listScrollingContainer.className = 'list-scrolling-container';
            window.listView.listScrollingContainer.id = 'list--container';
            listContainer.appendChild(window.listView.listScrollingContainer);
            window.listView.initialized = true;
        }
        if (!window.listView.initialized) {
           init();
        }

        let sortPlaces = [];
        sortPlaces = places.sort(window.PlacesSort[window.app.state.sortBy]);
        window.listView.sorting(sortPlaces);
        window.lazyload();
    },
    sorting: (places) => {
        if (typeof (window.listView.listScrollingContainer) != undefined && window.listView.listScrollingContainer != null) {
            window.listView.listScrollingContainer.querySelectorAll('*').forEach(node => node.remove());
        }
            places.forEach((place, index) => {

                if (!place.address || !place.address.lat || !place.address.lng) {
                    return;
                }

                const listItem = document.createElement('div');
                listItem.setAttribute('style', `${window.listView.imageHeight}px !important`);
                listItem.id = (place.id) ? `id_${place.id}` : '';
                listItem.className = 'list-item';

                listItem.addEventListener('click', e => {
                    e.preventDefault();
                    window.app.state.selectedPlace.unshift(place);
                    window.router.navigate(window.app.settings.viewStates.detail);
                });

                //Add Image
                const listImage = place.image ? place.image : window.listView.defaultImage;
                const image = document.createElement('img');

                image.setAttribute('data-src', window.listView.imagePrefix + listImage);
                image.setAttribute('width', window.listView.imageWidth);
                image.setAttribute('height', window.listView.imageHeight);
                image.setAttribute('style', `${window.listView.imageHeight}px !important`);
                image.className = 'lazyload';

                const infoContainer = document.createElement('div');
                infoContainer.className = 'list-info-container';

                const title = document.createElement('div');
                title.className = 'list-title';
                title.innerHTML = place.title;
                infoContainer.appendChild(title);

                const subtitle = document.createElement('div');
                let subtitleText = (place.subtitle && place.subtitle.length)
                    ? place.subtitle : '';

                subtitle.className = 'list-description';
                subtitle.innerHTML = subtitleText;
                infoContainer.appendChild(subtitle);

                const viewBtn = document.createElement('img');
                viewBtn.className = 'list-view-btn';
                viewBtn.src = 'images/right-arrow.png';
                infoContainer.appendChild(viewBtn);

                const address = document.createElement('div');
                address.innerHTML = place.address;

                const distance = document.createElement('div');
                distance.setAttribute('id', `distance-${place.id}`);
                distance.innerHTML = (place.distance) ? place.distance : '';
                distance.className = 'list-distance';
                infoContainer.appendChild(distance);

                listItem.appendChild(image);
                listItem.appendChild(infoContainer);
                if (typeof (window.listView.listScrollingContainer) != undefined && window.listView.listScrollingContainer != null) {
                    window.listView.listScrollingContainer.appendChild(listItem);
                }
            });
    },
    initList: (places) => {
        //Add filter control
        let filterDiv = document.getElementById('filter');
        new window.FilterControl(filterDiv);
        window.listView.addPlaces(places);
    },
    updateList: (newPlaces) => {
        console.log('called updateList()');
        window.listView.addPlaces(newPlaces);
    },
    filter(placesToHide, placesToShow) {
        //Hide filtered places
        placesToHide.forEach((place) => {
            let divToHide = document.getElementById(`id_${place.id}`);
            if(divToHide)
                divToHide.setAttribute('style', 'display:none !important');
        });

        //Show places that have been hidden
        placesToShow.forEach((place) => {
            let divToShow = document.getElementById(`id_${place.id}`);
            if(divToShow)
                divToShow.setAttribute('style', 'display:block !important');
        });
    },
    updateDistances: (places) => {
        places.forEach((place, index) => {
            let distanceElement = document.getElementById(`distance-${place.id}`);

            if(distanceElement)
                distanceElement.innerHTML = place.distance;
        });
    }
};<|endoftext|>


<commit_before>'use strict';

const util = require('./util');
const buildOptions = require('./util').buildOptions;
const xmlNode = require('./xmlNode');
const toNumber = require("strnum");

const regx =
  '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
  .replace(/NAME/g, util.nameRegexp);

//const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g");
//const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g");

//polyfill
if (!Number.parseInt && window.parseInt) {
  Number.parseInt = window.parseInt;
}
if (!Number.parseFloat && window.parseFloat) {
  Number.parseFloat = window.parseFloat;
}

const defaultOptions = {
  attributeNamePrefix: '@_',
  attrNodeName: false,
  textNodeName: '#text',
  ignoreAttributes: true,
  ignoreNameSpace: false,
  allowBooleanAttributes: false, //a tag can have attributes without any value
  //ignoreRootElement : false,
  parseNodeValue: true,
  parseAttributeValue: false,
  trimValues: true, //Trim string values of tag and attributes
  cdataTagName: false,
  cdataPositionChar: '\\c',
  numParseOptions: {
    hex: true,
    leadingZeros: true
  },
  tagValueProcessor: function(tagName, a) {
    return a;
  },
  attrValueProcessor: function(a, attrName) {
    return a;
  },
  stopNodes: [],
  alwaysCreateTextNode: false,
  //decodeStrict: false,
  isArray: () => false
};

exports.defaultOptions = defaultOptions;

const props = [
  'attributeNamePrefix',
  'attrNodeName',
  'textNodeName',
  'ignoreAttributes',
  'ignoreNameSpace',
  'allowBooleanAttributes',
  'parseNodeValue',
  'parseAttributeValue',
  'trimValues',
  'cdataTagName',
  'cdataPositionChar',
  'tagValueProcessor',
  'attrValueProcessor',
  'parseTrueNumberOnly',
  'numParseOptions',
  'stopNodes', //done
  'alwaysCreateTextNode', //done
  'isArray', //done
];
exports.props = props;

/**
 * Trim
 * @param {string} val
 * @param {object} options
 */
function parseValue(val, options, tagName, jPath) {
  if (val) {
    if (options.trimValues) {
      val = val.trim();
    }
    if(val.length > 0){
      const newval = options.tagValueProcessor(tagName, val, jPath);
      if(newval === null || newval === undefined){
        //don't parse
        return val;
      }else if(typeof newval !== typeof val || newval !== val){
        //don't prase
        return newval;
      }else{
        return _parseValue(val, options.parseNodeValue, options.numParseOptions);
      }
    }
  }
}

function resolveNameSpace(tagname, options) {
  if (options.ignoreNameSpace) {
    const tags = tagname.split(':');
    const prefix = tagname.charAt(0) === '/' ? '/' : '';
    if (tags[0] === 'xmlns') {
      return '';
    }
    if (tags.length === 2) {
      tagname = prefix + tags[1];
    }
  }
  return tagname;
}

function _parseValue(val, shouldParse, options) {
  if (shouldParse && typeof val === 'string') {
    //console.log(options)
    const newval = val.trim();
    if(newval === 'true' ) return true;
    else if(newval === 'false' ) return false;
    else return toNumber(val, options);
  } else {
    if (util.isExist(val)) {
      return val;
    } else {
      return '';
    }
  }
}

//TODO: change regex to capture NS
//const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm");
const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])(.*?)\\3)?', 'g');

function buildAttributesMap(attrStr, options) {
  if (!options.ignoreAttributes && typeof attrStr === 'string') {
    attrStr = attrStr.replace(/\r?\n/g, ' ');
    //attrStr = attrStr || attrStr.trim();

    const matches = util.getAllMatches(attrStr, attrsRegx);
    const len = matches.length; //don't make it inline
    const attrs = {};
    for (let i = 0; i < len; i++) {
      const attrName = resolveNameSpace(matches[i][1], options);
      if (attrName.length) {
        if (matches[i][4] !== undefined) {
          if (options.trimValues) {
            matches[i][4] = matches[i][4].trim();
          }
          matches[i][4] = options.attrValueProcessor(matches[i][4], attrName);
          attrs[options.attributeNamePrefix + attrName] = _parseValue(
            matches[i][4],
            options.parseAttributeValue,
            options.numParseOptions
          );
        } else if (options.allowBooleanAttributes) {
          attrs[options.attributeNamePrefix + attrName] = true;
        }
      }
    }
    if (!Object.keys(attrs).length) {
      return;
    }
    if (options.attrNodeName) {
      const attrCollection = {};
      attrCollection[options.attrNodeName] = attrs;
      return attrCollection;
    }
    return attrs;
  }
}

const getTraversalObj = function(xmlData, options) {
  xmlData = xmlData.replace(/\r\n?/g, "\n");
  options = buildOptions(options, defaultOptions, props);
  const xmlObj = new xmlNode('!xml');
  let currentNode = xmlObj;
  let textData = "";
  const tagsNodeStack = [];
  let jPath = "";

  for(let i=0; i< xmlData.length; i++){//for each char in XML data
    const ch = xmlData[i];
    if(ch === '<'){
      // const nextIndex = i+1;
      // const _2ndChar = xmlData[nextIndex];
      if( xmlData[i+1] === '/') {//Closing Tag
        const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.")
        let tagName = xmlData.substring(i+2,closeIndex).trim();

        if(options.ignoreNameSpace){
          const colonIndex = tagName.indexOf(":");
          if(colonIndex !== -1){
            tagName = tagName.substr(colonIndex+1);
          }
        }

        if(currentNode){
          textData = parseValue(textData, options, currentNode.tagname, jPath)
          if(textData) currentNode.add(options.textNodeName, textData);
          textData = "";
        }

        if (isItStopNode(options.stopNodes, tagsNodeStack, currentNode.tagname)) { //TODO: namespace
          const top = tagsNodeStack[tagsNodeStack.length - 1];
          const stopNode = top.child[ top.child.length -1 ];
          stopNode[currentNode.tagname] = [ { [options.textNodeName] :xmlData.substr(currentNode.startIndex + 1, i - currentNode.startIndex - 1) }];
        }

        jPath = jPath.substr(0, jPath.lastIndexOf("."));

        currentNode = tagsNodeStack.pop();//avoid recurssion, set the parent tag scope
        textData = "";
        i = closeIndex;
      } else if( xmlData[i+1] === '?') {
        i = findClosingIndex(xmlData, "?>", i, "Pi Tag is not closed.")
      } else if(xmlData.substr(i + 1, 3) === '!--') {
        i = findClosingIndex(xmlData, "-->", i, "Comment is not closed.")
      } else if( xmlData.substr(i + 1, 2) === '!D') {
        const closeIndex = findClosingIndex(xmlData, ">", i, "DOCTYPE is not closed.")
        const tagExp = xmlData.substring(i, closeIndex);
        if(tagExp.indexOf("[") >= 0){
          i = xmlData.indexOf("]>", i) + 1;
        }else{
          i = closeIndex;
        }
      }else if(xmlData.substr(i + 1, 2) === '![') {
        const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
        const tagExp = xmlData.substring(i + 9,closeIndex);

        if(textData){ //store previously collected data as textNode
          textData = parseValue(textData, options, currentNode.tagname, jPath);
          if(textData) currentNode.add(options.textNodeName, textData);
          textData = "";
        }

        if(options.cdataTagName){
          currentNode.add(options.cdataTagName, [ { [options.textNodeName] : parseValue(tagExp, options, options.cdataTagName, jPath + "." + options.cdataTagName) } ]);
        }else{
          currentNode.add(options.textNodeName, parseValue(tagExp, options, currentNode.tagname, jPath));
        }

        i = closeIndex + 2;
      }else {//Opening tag
        const result = tagExpWithClosingIndex(xmlData, i+1)
        let tagExp = result.data;
        const closeIndex = result.index;
        const separatorIndex = tagExp.indexOf(" ");
        let tagName = tagExp;
        let shouldBuildAttributesMap = true;
        if(separatorIndex !== -1){//separate tag name and attributes expression
          tagName = tagExp.substr(0, separatorIndex).replace(/\s\s*$/, '');
          tagExp = tagExp.substr(separatorIndex + 1);
        }

        if(options.ignoreNameSpace){
          const colonIndex = tagName.indexOf(":");
          if(colonIndex !== -1){
            tagName = tagName.substr(colonIndex+1);
            shouldBuildAttributesMap = tagName !== result.data.substr(colonIndex + 1);
          }
        }
        if(tagName !== xmlObj.tagname){
          jPath += jPath ? "." + tagName : tagName;
        }
        //save text as child node
        if (currentNode && textData) {
          if(currentNode.tagname !== '!xml'){
            textData = parseValue(textData, options, currentNode.tagname, jPath );
            if(textData) currentNode.add(options.textNodeName, textData);
            textData = "";
          }
        }

        if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){//selfClosing tag

          if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
            tagName = tagName.substr(0, tagName.length - 1);
            tagExp = tagName;
          }else{
            tagExp = tagExp.substr(0, tagExp.length - 1);
          }

          const childNode = new xmlNode(tagName);
          if(tagName !== tagExp && shouldBuildAttributesMap){
            childNode.attrsMap = buildAttributesMap(tagExp, options);
          }
          tagsNodeStack.push(currentNode);
          currentNode.addChild(childNode);
        }else{//opening tag

          const childNode = new xmlNode( tagName);
          tagsNodeStack.push(currentNode);

          childNode.startIndex=closeIndex; //for further processing

          if(tagName !== tagExp && shouldBuildAttributesMap){
            childNode.attrsMap = buildAttributesMap(tagExp, options);
          }
          currentNode.addChild(childNode);
          currentNode = childNode;
        }
        textData = "";
        i = closeIndex;
      }
    }else{
      textData += xmlData[i];
    }
  }
  return xmlObj.child[0];
}

/**
 *
 * @param {string[]} stopNodes
 * @param {XmlNode[]} tagsNodeStack
 */
function isItStopNode(stopNodes, tagsNodeStack, currentTagName){
  const matchingStopNodes = [];
  //filter the list of stopNodes as per current tag
  stopNodes.forEach( jPath => {
    if( jPath.substr( jPath.length - currentTagName.length) === currentTagName) matchingStopNodes.push(jPath);
  });

  if(matchingStopNodes.length > 0){
    let jPath = "";
    for (let i = 1; i < tagsNodeStack.length; i++) {
      const node = tagsNodeStack[i];
      jPath += "." + node.tagname;
    }
    jPath += "." + currentTagName;
    jPath = jPath.substr(1);
    for (let i = 0; i < matchingStopNodes.length; i++) {
      if(matchingStopNodes[i] === jPath) return true;
    }
  }else return false;
}

/**
 * Returns the tag Expression and where it is ending handling single-dobule quotes situation
 * @param {string} xmlData
 * @param {number} i starting index
 * @returns
 */
function tagExpWithClosingIndex(xmlData, i){
  let attrBoundary;
  let tagExp = "";
  for (let index = i; index < xmlData.length; index++) {
    let ch = xmlData[index];
    if (attrBoundary) {
        if (ch === attrBoundary) attrBoundary = "";//reset
    } else if (ch === '"' || ch === "'") {
        attrBoundary = ch;
    } else if (ch === '>') {
        return {
          data: tagExp,
          index: index
        }
    } else if (ch === '\t') {
      ch = " "
    }
    tagExp += ch;
  }
}

function findClosingIndex(xmlData, str, i, errMsg){
  const closingIndex = xmlData.indexOf(str, i);
  if(closingIndex === -1){
    throw new Error(errMsg)
  }else{
    return closingIndex + str.length - 1;
  }
}

exports.getTraversalObj = getTraversalObj;
<commit_msg>attrValueProcessor<commit_after>'use strict';

const util = require('./util');
const buildOptions = require('./util').buildOptions;
const xmlNode = require('./xmlNode');
const toNumber = require("strnum");

const regx =
  '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)'
  .replace(/NAME/g, util.nameRegexp);

//const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g");
//const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g");

//polyfill
if (!Number.parseInt && window.parseInt) {
  Number.parseInt = window.parseInt;
}
if (!Number.parseFloat && window.parseFloat) {
  Number.parseFloat = window.parseFloat;
}

const defaultOptions = {
  attributeNamePrefix: '@_',
  attrNodeName: false,
  textNodeName: '#text',
  ignoreAttributes: true,
  ignoreNameSpace: false,
  allowBooleanAttributes: false, //a tag can have attributes without any value
  //ignoreRootElement : false,
  parseNodeValue: true,
  parseAttributeValue: false,
  trimValues: true, //Trim string values of tag and attributes
  cdataTagName: false,
  cdataPositionChar: '\\c',
  numParseOptions: {
    hex: true,
    leadingZeros: true
  },
  tagValueProcessor: function(tagName, a) {
    return a;
  },
  attrValueProcessor: function(a, attrName) {
    return a;
  },
  stopNodes: [],
  alwaysCreateTextNode: false,
  //decodeStrict: false,
  isArray: () => false
};

exports.defaultOptions = defaultOptions;

const props = [
  'attributeNamePrefix',
  'attrNodeName',
  'textNodeName',
  'ignoreAttributes',
  'ignoreNameSpace',
  'allowBooleanAttributes',
  'parseNodeValue',
  'parseAttributeValue',
  'trimValues',
  'cdataTagName',
  'cdataPositionChar',
  'tagValueProcessor',
  'attrValueProcessor',
  'parseTrueNumberOnly',
  'numParseOptions',
  'stopNodes', //done
  'alwaysCreateTextNode', //done
  'isArray', //done
];
exports.props = props;

/**
 * Trim
 * @param {string} val
 * @param {object} options
 */
function parseValue(val, options, tagName, jPath) {
  if (val) {
    if (options.trimValues) {
      val = val.trim();
    }
    if(val.length > 0){
      const newval = options.tagValueProcessor(tagName, val, jPath);
      if(newval === null || newval === undefined){
        //don't parse
        return val;
      }else if(typeof newval !== typeof val || newval !== val){
        //overwrite
        return newval;
      }else{
        return _parseValue(val, options.parseNodeValue, options.numParseOptions);
      }
    }
  }
}

function resolveNameSpace(tagname, options) {
  if (options.ignoreNameSpace) {
    const tags = tagname.split(':');
    const prefix = tagname.charAt(0) === '/' ? '/' : '';
    if (tags[0] === 'xmlns') {
      return '';
    }
    if (tags.length === 2) {
      tagname = prefix + tags[1];
    }
  }
  return tagname;
}

function _parseValue(val, shouldParse, options) {
  if (shouldParse && typeof val === 'string') {
    //console.log(options)
    const newval = val.trim();
    if(newval === 'true' ) return true;
    else if(newval === 'false' ) return false;
    else return toNumber(val, options);
  } else {
    if (util.isExist(val)) {
      return val;
    } else {
      return '';
    }
  }
}

//TODO: change regex to capture NS
//const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm");
const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])(.*?)\\3)?', 'g');

function buildAttributesMap(attrStr, jPath, options) {
  if (!options.ignoreAttributes && typeof attrStr === 'string') {
    attrStr = attrStr.replace(/\r?\n/g, ' ');
    //attrStr = attrStr || attrStr.trim();

    const matches = util.getAllMatches(attrStr, attrsRegx);
    const len = matches.length; //don't make it inline
    const attrs = {};
    for (let i = 0; i < len; i++) {
      const attrName = resolveNameSpace(matches[i][1], options);
      let oldVal = matches[i][4];
      const aName = options.attributeNamePrefix + attrName;
      if (attrName.length) {
        if (oldVal !== undefined) {
          if (options.trimValues) {
            oldVal = oldVal.trim();
          }

          const newVal = options.attrValueProcessor(attrName, oldVal, jPath);
          if(newVal === null || newVal === undefined){
            //don't parse
            attrs[aName] = oldVal;
          }else if(typeof newVal !== typeof oldVal || newVal !== oldVal){
            //overwrite
            attrs[aName] = newVal;
          }else{
            //parse
            attrs[aName] = _parseValue(
              oldVal,
              options.parseAttributeValue,
              options.numParseOptions
            );
          }
        } else if (options.allowBooleanAttributes) {
          attrs[aName] = true;
        }
      }
    }
    if (!Object.keys(attrs).length) {
      return;
    }
    if (options.attrNodeName) {
      const attrCollection = {};
      attrCollection[options.attrNodeName] = attrs;
      return attrCollection;
    }
    return attrs;
  }
}

const getTraversalObj = function(xmlData, options) {
  xmlData = xmlData.replace(/\r\n?/g, "\n");
  options = buildOptions(options, defaultOptions, props);
  const xmlObj = new xmlNode('!xml');
  let currentNode = xmlObj;
  let textData = "";
  const tagsNodeStack = [];
  let jPath = "";

  for(let i=0; i< xmlData.length; i++){//for each char in XML data
    const ch = xmlData[i];
    if(ch === '<'){
      // const nextIndex = i+1;
      // const _2ndChar = xmlData[nextIndex];
      if( xmlData[i+1] === '/') {//Closing Tag
        const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.")
        let tagName = xmlData.substring(i+2,closeIndex).trim();

        if(options.ignoreNameSpace){
          const colonIndex = tagName.indexOf(":");
          if(colonIndex !== -1){
            tagName = tagName.substr(colonIndex+1);
          }
        }

        if(currentNode){
          textData = parseValue(textData, options, currentNode.tagname, jPath)
          if(textData) currentNode.add(options.textNodeName, textData);
          textData = "";
        }

        if (isItStopNode(options.stopNodes, tagsNodeStack, currentNode.tagname)) { //TODO: namespace
          const top = tagsNodeStack[tagsNodeStack.length - 1];
          const stopNode = top.child[ top.child.length -1 ];
          stopNode[currentNode.tagname] = [ { [options.textNodeName] :xmlData.substr(currentNode.startIndex + 1, i - currentNode.startIndex - 1) }];
        }

        jPath = jPath.substr(0, jPath.lastIndexOf("."));

        currentNode = tagsNodeStack.pop();//avoid recurssion, set the parent tag scope
        textData = "";
        i = closeIndex;
      } else if( xmlData[i+1] === '?') {
        i = findClosingIndex(xmlData, "?>", i, "Pi Tag is not closed.")
      } else if(xmlData.substr(i + 1, 3) === '!--') {
        i = findClosingIndex(xmlData, "-->", i, "Comment is not closed.")
      } else if( xmlData.substr(i + 1, 2) === '!D') {
        const closeIndex = findClosingIndex(xmlData, ">", i, "DOCTYPE is not closed.")
        const tagExp = xmlData.substring(i, closeIndex);
        if(tagExp.indexOf("[") >= 0){
          i = xmlData.indexOf("]>", i) + 1;
        }else{
          i = closeIndex;
        }
      }else if(xmlData.substr(i + 1, 2) === '![') {
        const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2;
        const tagExp = xmlData.substring(i + 9,closeIndex);

        if(textData){ //store previously collected data as textNode
          textData = parseValue(textData, options, currentNode.tagname, jPath);
          if(textData) currentNode.add(options.textNodeName, textData);
          textData = "";
        }

        if(options.cdataTagName){
          currentNode.add(options.cdataTagName, [ { [options.textNodeName] : parseValue(tagExp, options, options.cdataTagName, jPath + "." + options.cdataTagName) } ]);
        }else{
          currentNode.add(options.textNodeName, parseValue(tagExp, options, currentNode.tagname, jPath));
        }

        i = closeIndex + 2;
      }else {//Opening tag
        const result = tagExpWithClosingIndex(xmlData, i+1)
        let tagExp = result.data;
        const closeIndex = result.index;
        const separatorIndex = tagExp.indexOf(" ");
        let tagName = tagExp;
        let shouldBuildAttributesMap = true;
        if(separatorIndex !== -1){//separate tag name and attributes expression
          tagName = tagExp.substr(0, separatorIndex).replace(/\s\s*$/, '');
          tagExp = tagExp.substr(separatorIndex + 1);
        }

        if(options.ignoreNameSpace){
          const colonIndex = tagName.indexOf(":");
          if(colonIndex !== -1){
            tagName = tagName.substr(colonIndex+1);
            shouldBuildAttributesMap = tagName !== result.data.substr(colonIndex + 1);
          }
        }
        if(tagName !== xmlObj.tagname){
          jPath += jPath ? "." + tagName : tagName;
        }
        //save text as child node
        if (currentNode && textData) {
          if(currentNode.tagname !== '!xml'){
            textData = parseValue(textData, options, currentNode.tagname, jPath );
            if(textData) currentNode.add(options.textNodeName, textData);
            textData = "";
          }
        }

        if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){//selfClosing tag

          if(tagName[tagName.length - 1] === "/"){ //remove trailing '/'
            tagName = tagName.substr(0, tagName.length - 1);
            tagExp = tagName;
          }else{
            tagExp = tagExp.substr(0, tagExp.length - 1);
          }

          const childNode = new xmlNode(tagName);
          if(tagName !== tagExp && shouldBuildAttributesMap){
            childNode.attrsMap = buildAttributesMap(tagExp, jPath , options);
          }
          jPath = jPath.substr(0, jPath.lastIndexOf("."));
          tagsNodeStack.push(currentNode);
          currentNode.addChild(childNode);
        }else{//opening tag

          const childNode = new xmlNode( tagName);
          tagsNodeStack.push(currentNode);

          childNode.startIndex=closeIndex; //for further processing

          if(tagName !== tagExp && shouldBuildAttributesMap){
            childNode.attrsMap = buildAttributesMap(tagExp, jPath, options);
          }
          currentNode.addChild(childNode);
          currentNode = childNode;
        }
        textData = "";
        i = closeIndex;
      }
    }else{
      textData += xmlData[i];
    }
  }
  return xmlObj.child[0];
}

/**
 *
 * @param {string[]} stopNodes
 * @param {XmlNode[]} tagsNodeStack
 */
function isItStopNode(stopNodes, tagsNodeStack, currentTagName){
  const matchingStopNodes = [];
  //filter the list of stopNodes as per current tag
  stopNodes.forEach( jPath => {
    if( jPath.substr( jPath.length - currentTagName.length) === currentTagName) matchingStopNodes.push(jPath);
  });

  if(matchingStopNodes.length > 0){
    let jPath = "";
    for (let i = 1; i < tagsNodeStack.length; i++) {
      const node = tagsNodeStack[i];
      jPath += "." + node.tagname;
    }
    jPath += "." + currentTagName;
    jPath = jPath.substr(1);
    for (let i = 0; i < matchingStopNodes.length; i++) {
      if(matchingStopNodes[i] === jPath) return true;
    }
  }else return false;
}

/**
 * Returns the tag Expression and where it is ending handling single-dobule quotes situation
 * @param {string} xmlData
 * @param {number} i starting index
 * @returns
 */
function tagExpWithClosingIndex(xmlData, i){
  let attrBoundary;
  let tagExp = "";
  for (let index = i; index < xmlData.length; index++) {
    let ch = xmlData[index];
    if (attrBoundary) {
        if (ch === attrBoundary) attrBoundary = "";//reset
    } else if (ch === '"' || ch === "'") {
        attrBoundary = ch;
    } else if (ch === '>') {
        return {
          data: tagExp,
          index: index
        }
    } else if (ch === '\t') {
      ch = " "
    }
    tagExp += ch;
  }
}

function findClosingIndex(xmlData, str, i, errMsg){
  const closingIndex = xmlData.indexOf(str, i);
  if(closingIndex === -1){
    throw new Error(errMsg)
  }else{
    return closingIndex + str.length - 1;
  }
}

exports.getTraversalObj = getTraversalObj;
<|endoftext|>


<commit_before>///// represents a single document

var haste_document = function() {
  this.locked = false;
};

// Escapes HTML tag characters
haste_document.prototype.htmlEscape = function(s) {
  return s
    .replace(/&/g, '&')
    .replace(/>/g, '>')
    .replace(/</g, '<')
    .replace(/"/g, '"');
};

// Get this document from the server and lock it here
haste_document.prototype.load = function(key, callback, lang) {
  var _this = this;
  $.ajax('/documents/' + key, {
    type: 'get',
    dataType: 'json',
    success: function(res) {
      _this.locked = true;
      _this.key = key;
      _this.data = res.data;
      try {
        var high;
        if (lang === 'txt') {
          high = { value: _this.htmlEscape(res.data) };
        }
        else if (lang) {
          high = hljs.highlight(lang, res.data);
        }
        else {
          high = hljs.highlightAuto(res.data);
        }
      } catch(err) {
        // failed highlight, fall back on auto
        high = hljs.highlightAuto(res.data);
      }
      callback({
        value: high.value,
        key: key,
        language: high.language || lang,
        lineCount: res.data.split("\n").length
      });
    },
    error: function(err) {
      callback(false);
    }
  });
};

// Save this document to the server and lock it here
haste_document.prototype.save = function(data, callback) {
  if (this.locked) {
    return false;
  }
  this.data = data;
  var _this = this;
  $.ajax('/documents', {
    type: 'post',
    data: data,
    dataType: 'json',
    contentType: 'application/json; charset=utf-8',
    success: function(res) {
      _this.locked = true;
      _this.key = res.key;
      var high = hljs.highlightAuto(data);
      callback(null, {
        value: high.value,
        key: res.key,
        language: high.language,
        lineCount: data.split("\n").length
      });
    },
    error: function(res) {
      try {
        callback($.parseJSON(res.responseText));
      }
      catch (e) {
        callback({message: 'Something went wrong!'});
      }
    }
  });
};

///// represents the paste application

var haste = function(appName, options) {
  this.appName = appName;
  this.$textarea = $('textarea');
  this.$box = $('#box');
  this.$code = $('#box code');
  this.$linenos = $('#linenos');
  this.options = options;
  this.configureShortcuts();
  this.configureButtons();
  // If twitter is disabled, hide the button
  if (!options.twitter) {
    $('#box2 .twitter').hide();
  }
};

// Set the page title - include the appName
haste.prototype.setTitle = function(ext) {
  var title = ext ? this.appName + ' - ' + ext : this.appName;
  document.title = title;
};

// Show a message box
haste.prototype.showMessage = function(msg, cls) {
  var msgBox = $('
'+msg+'
');
  $('#messages').prepend(msgBox);
  setTimeout(function() {
    msgBox.slideUp('fast', function() { $(this).remove(); });
  }, 3000);
};

// Show the light key
haste.prototype.lightKey = function() {
  this.configureKey(['new', 'save']);
};

// Show the full key
haste.prototype.fullKey = function() {
  this.configureKey(['new', 'duplicate', 'twitter', 'raw']);
};

// Set the key up for certain things to be enabled
haste.prototype.configureKey = function(enable) {
  var $this, i = 0;
  $('#box2 .function').each(function() {
    $this = $(this);
    for (i = 0; i < enable.length; i++) {
      if ($this.hasClass(enable[i])) {
        $this.addClass('enabled');
        return true;
      }
    }
    $this.removeClass('enabled');
  });
};

// Remove the current document (if there is one)
// and set up for a new one
haste.prototype.newDocument = function(hideHistory) {
  this.$box.hide();
  this.doc = new haste_document();
  if (!hideHistory) {
    window.history.pushState(null, this.appName, '/');
  }
  this.setTitle();
  this.lightKey();
  this.$textarea.val('').show('fast', function() {
    this.focus();
  });
  this.removeLineNumbers();
};

// Map of common extensions
// Note: this list does not need to include anything that IS its extension,
// due to the behavior of lookupTypeByExtension and lookupExtensionByType
// Note: optimized for lookupTypeByExtension
haste.extensionMap = {
  rb: 'ruby', py: 'python', pl: 'perl', php: 'php', scala: 'scala', go: 'go',
  xml: 'xml', html: 'xml', htm: 'xml', css: 'css', js: 'javascript', vbs: 'vbscript',
  lua: 'lua', pas: 'delphi', java: 'java', cpp: 'cpp', cc: 'cpp', m: 'objectivec',
  vala: 'vala', cs: 'cs', sql: 'sql', sm: 'smalltalk', lisp: 'lisp', ini: 'ini',
  diff: 'diff', bash: 'bash', sh: 'bash', tex: 'tex', erl: 'erlang', hs: 'haskell',
  md: 'markdown', txt: '', coffee: 'coffee', json: 'javascript'
};

// Look up the extension preferred for a type
// If not found, return the type itself - which we'll place as the extension
haste.prototype.lookupExtensionByType = function(type) {
  for (var key in haste.extensionMap) {
    if (haste.extensionMap[key] === type) return key;
  }
  return type;
};

// Look up the type for a given extension
// If not found, return the extension - which we'll attempt to use as the type
haste.prototype.lookupTypeByExtension = function(ext) {
  return haste.extensionMap[ext] || ext;
};

// Add line numbers to the document
// For the specified number of lines
haste.prototype.addLineNumbers = function(lineCount) {
  var h = '';
  for (var i = 0; i < lineCount; i++) {
    h += (i + 1).toString() + '
';
  }
  $('#linenos').html(h);
};

// Remove the line numbers
haste.prototype.removeLineNumbers = function() {
  $('#linenos').html('>');
};

// Load a document and show it
haste.prototype.loadDocument = function(key) {
  // Split the key up
  var parts = key.split('.', 2);
  // Ask for what we want
  var _this = this;
  _this.doc = new haste_document();
  _this.doc.load(parts[0], function(ret) {
    if (ret) {
      _this.$code.html(ret.value);
      _this.setTitle(ret.key);
      _this.fullKey();
      _this.$textarea.val('').hide();
      _this.$box.show().focus();
      _this.addLineNumbers(ret.lineCount);
    }
    else {
      _this.newDocument();
    }
  }, this.lookupTypeByExtension(parts[1]));
};

// Duplicate the current document - only if locked
haste.prototype.duplicateDocument = function() {
  if (this.doc.locked) {
    var currentData = this.doc.data;
    this.newDocument();
    this.$textarea.val(currentData);
  }
};

// Lock the current document
haste.prototype.lockDocument = function() {
  var _this = this;
  this.doc.save(this.$textarea.val(), function(err, ret) {
    if (err) {
      _this.showMessage(err.message, 'error');
    }
    else if (ret) {
      _this.$code.html(ret.value);
      _this.setTitle(ret.key);
      var file = '/' + ret.key;
      if (ret.language) {
        file += '.' + _this.lookupExtensionByType(ret.language);
      }
      window.history.pushState(null, _this.appName + '-' + ret.key, file);
      _this.fullKey();
      _this.$textarea.val('').hide();
      _this.$box.show().focus();
      _this.addLineNumbers(ret.lineCount);
    }
  });
};

haste.prototype.configureButtons = function() {
  var _this = this;
  this.buttons = [
    {
      $where: $('#box2 .save'),
      label: 'Save',
      shortcutDescription: 'control + s',
      shortcut: function(evt) {
        return evt.ctrlKey && (evt.keyCode === 83);
      },
      action: function() {
        if (_this.$textarea.val().replace(/^\s+|\s+$/g, '') !== '') {
          _this.lockDocument();
        }
      }
    },
    {
      $where: $('#box2 .new'),
      label: 'New',
      shortcut: function(evt) {
        return evt.ctrlKey && evt.keyCode === 78
      },
      shortcutDescription: 'control + n',
      action: function() {
        _this.newDocument(!_this.doc.key);
      }
    },
    {
      $where: $('#box2 .duplicate'),
      label: 'Duplicate & Edit',
      shortcut: function(evt) {
        return _this.doc.locked && evt.ctrlKey && evt.keyCode === 68;
      },
      shortcutDescription: 'control + d',
      action: function() {
        _this.duplicateDocument();
      }
    },
    {
      $where: $('#box2 .raw'),
      label: 'Just Text',
      shortcut: function(evt) {
        return evt.ctrlKey && evt.shiftKey && evt.keyCode === 82;
      },
      shortcutDescription: 'control + shift + r',
      action: function() {
        window.location.href = '/raw/' + _this.doc.key;
      }
    },
    {
      $where: $('#box2 .twitter'),
      label: 'Twitter',
      shortcut: function(evt) {
        return _this.options.twitter && _this.doc.locked && evt.ctrlKey && evt.keyCode == 84;
      },
      shortcutDescription: 'control + t',
      action: function() {
        window.open('https://twitter.com/share?url=' + encodeURI(window.location.href));
      }
    }
  ];
  for (var i = 0; i < this.buttons.length; i++) {
    this.configureButton(this.buttons[i]);
  }
};

haste.prototype.configureButton = function(options) {
  // Handle the click action
  options.$where.click(function(evt) {
    evt.preventDefault();
    if (!options.clickDisabled && $(this).hasClass('enabled')) {
      options.action();
    }
  });
  // Show the label
  options.$where.mouseenter(function(evt) {
    $('#box3 .label').text(options.label);
    $('#box3 .shortcut').text(options.shortcutDescription || '');
    $('#box3').show();
    $(this).append($('#pointer').remove().show());
  });
  // Hide the label
  options.$where.mouseleave(function(evt) {
    $('#box3').hide();
    $('#pointer').hide();
  });
};

// Configure keyboard shortcuts for the textarea
haste.prototype.configureShortcuts = function() {
  var _this = this;
  $(document.body).keydown(function(evt) {
    var button;
    for (var i = 0 ; i < _this.buttons.length; i++) {
      button = _this.buttons[i];
      if (button.shortcut && button.shortcut(evt)) {
        evt.preventDefault();
        button.action();
        return;
      }
    }
  });
};

///// Tab behavior in the textarea - 2 spaces per tab
$(function() {

  $('textarea').keydown(function(evt) {
    if (evt.keyCode === 9) {
      evt.preventDefault();
      var myValue = '  ';
      // http://stackoverflow.com/questions/946534/insert-text-into-textarea-with-jquery
      // For browsers like Internet Explorer
      if (document.selection) {
        this.focus();
        sel = document.selection.createRange();
        sel.text = myValue;
        this.focus();
      }
      // Mozilla and Webkit
      else if (this.selectionStart || this.selectionStart == '0') {
        var startPos = this.selectionStart;
        var endPos = this.selectionEnd;
        var scrollTop = this.scrollTop;
        this.value = this.value.substring(0, startPos) + myValue +
          this.value.substring(endPos,this.value.length);
        this.focus();
        this.selectionStart = startPos + myValue.length;
        this.selectionEnd = startPos + myValue.length;
        this.scrollTop = scrollTop;
      }
      else {
        this.value += myValue;
        this.focus();
      }
    }
  });

});
<commit_msg>Added shift modifier to twitter shortcut<commit_after>///// represents a single document

var haste_document = function() {
  this.locked = false;
};

// Escapes HTML tag characters
haste_document.prototype.htmlEscape = function(s) {
  return s
    .replace(/&/g, '&')
    .replace(/>/g, '>')
    .replace(/</g, '<')
    .replace(/"/g, '"');
};

// Get this document from the server and lock it here
haste_document.prototype.load = function(key, callback, lang) {
  var _this = this;
  $.ajax('/documents/' + key, {
    type: 'get',
    dataType: 'json',
    success: function(res) {
      _this.locked = true;
      _this.key = key;
      _this.data = res.data;
      try {
        var high;
        if (lang === 'txt') {
          high = { value: _this.htmlEscape(res.data) };
        }
        else if (lang) {
          high = hljs.highlight(lang, res.data);
        }
        else {
          high = hljs.highlightAuto(res.data);
        }
      } catch(err) {
        // failed highlight, fall back on auto
        high = hljs.highlightAuto(res.data);
      }
      callback({
        value: high.value,
        key: key,
        language: high.language || lang,
        lineCount: res.data.split("\n").length
      });
    },
    error: function(err) {
      callback(false);
    }
  });
};

// Save this document to the server and lock it here
haste_document.prototype.save = function(data, callback) {
  if (this.locked) {
    return false;
  }
  this.data = data;
  var _this = this;
  $.ajax('/documents', {
    type: 'post',
    data: data,
    dataType: 'json',
    contentType: 'application/json; charset=utf-8',
    success: function(res) {
      _this.locked = true;
      _this.key = res.key;
      var high = hljs.highlightAuto(data);
      callback(null, {
        value: high.value,
        key: res.key,
        language: high.language,
        lineCount: data.split("\n").length
      });
    },
    error: function(res) {
      try {
        callback($.parseJSON(res.responseText));
      }
      catch (e) {
        callback({message: 'Something went wrong!'});
      }
    }
  });
};

///// represents the paste application

var haste = function(appName, options) {
  this.appName = appName;
  this.$textarea = $('textarea');
  this.$box = $('#box');
  this.$code = $('#box code');
  this.$linenos = $('#linenos');
  this.options = options;
  this.configureShortcuts();
  this.configureButtons();
  // If twitter is disabled, hide the button
  if (!options.twitter) {
    $('#box2 .twitter').hide();
  }
};

// Set the page title - include the appName
haste.prototype.setTitle = function(ext) {
  var title = ext ? this.appName + ' - ' + ext : this.appName;
  document.title = title;
};

// Show a message box
haste.prototype.showMessage = function(msg, cls) {
  var msgBox = $('
'+msg+'
');
  $('#messages').prepend(msgBox);
  setTimeout(function() {
    msgBox.slideUp('fast', function() { $(this).remove(); });
  }, 3000);
};

// Show the light key
haste.prototype.lightKey = function() {
  this.configureKey(['new', 'save']);
};

// Show the full key
haste.prototype.fullKey = function() {
  this.configureKey(['new', 'duplicate', 'twitter', 'raw']);
};

// Set the key up for certain things to be enabled
haste.prototype.configureKey = function(enable) {
  var $this, i = 0;
  $('#box2 .function').each(function() {
    $this = $(this);
    for (i = 0; i < enable.length; i++) {
      if ($this.hasClass(enable[i])) {
        $this.addClass('enabled');
        return true;
      }
    }
    $this.removeClass('enabled');
  });
};

// Remove the current document (if there is one)
// and set up for a new one
haste.prototype.newDocument = function(hideHistory) {
  this.$box.hide();
  this.doc = new haste_document();
  if (!hideHistory) {
    window.history.pushState(null, this.appName, '/');
  }
  this.setTitle();
  this.lightKey();
  this.$textarea.val('').show('fast', function() {
    this.focus();
  });
  this.removeLineNumbers();
};

// Map of common extensions
// Note: this list does not need to include anything that IS its extension,
// due to the behavior of lookupTypeByExtension and lookupExtensionByType
// Note: optimized for lookupTypeByExtension
haste.extensionMap = {
  rb: 'ruby', py: 'python', pl: 'perl', php: 'php', scala: 'scala', go: 'go',
  xml: 'xml', html: 'xml', htm: 'xml', css: 'css', js: 'javascript', vbs: 'vbscript',
  lua: 'lua', pas: 'delphi', java: 'java', cpp: 'cpp', cc: 'cpp', m: 'objectivec',
  vala: 'vala', cs: 'cs', sql: 'sql', sm: 'smalltalk', lisp: 'lisp', ini: 'ini',
  diff: 'diff', bash: 'bash', sh: 'bash', tex: 'tex', erl: 'erlang', hs: 'haskell',
  md: 'markdown', txt: '', coffee: 'coffee', json: 'javascript'
};

// Look up the extension preferred for a type
// If not found, return the type itself - which we'll place as the extension
haste.prototype.lookupExtensionByType = function(type) {
  for (var key in haste.extensionMap) {
    if (haste.extensionMap[key] === type) return key;
  }
  return type;
};

// Look up the type for a given extension
// If not found, return the extension - which we'll attempt to use as the type
haste.prototype.lookupTypeByExtension = function(ext) {
  return haste.extensionMap[ext] || ext;
};

// Add line numbers to the document
// For the specified number of lines
haste.prototype.addLineNumbers = function(lineCount) {
  var h = '';
  for (var i = 0; i < lineCount; i++) {
    h += (i + 1).toString() + '
';
  }
  $('#linenos').html(h);
};

// Remove the line numbers
haste.prototype.removeLineNumbers = function() {
  $('#linenos').html('>');
};

// Load a document and show it
haste.prototype.loadDocument = function(key) {
  // Split the key up
  var parts = key.split('.', 2);
  // Ask for what we want
  var _this = this;
  _this.doc = new haste_document();
  _this.doc.load(parts[0], function(ret) {
    if (ret) {
      _this.$code.html(ret.value);
      _this.setTitle(ret.key);
      _this.fullKey();
      _this.$textarea.val('').hide();
      _this.$box.show().focus();
      _this.addLineNumbers(ret.lineCount);
    }
    else {
      _this.newDocument();
    }
  }, this.lookupTypeByExtension(parts[1]));
};

// Duplicate the current document - only if locked
haste.prototype.duplicateDocument = function() {
  if (this.doc.locked) {
    var currentData = this.doc.data;
    this.newDocument();
    this.$textarea.val(currentData);
  }
};

// Lock the current document
haste.prototype.lockDocument = function() {
  var _this = this;
  this.doc.save(this.$textarea.val(), function(err, ret) {
    if (err) {
      _this.showMessage(err.message, 'error');
    }
    else if (ret) {
      _this.$code.html(ret.value);
      _this.setTitle(ret.key);
      var file = '/' + ret.key;
      if (ret.language) {
        file += '.' + _this.lookupExtensionByType(ret.language);
      }
      window.history.pushState(null, _this.appName + '-' + ret.key, file);
      _this.fullKey();
      _this.$textarea.val('').hide();
      _this.$box.show().focus();
      _this.addLineNumbers(ret.lineCount);
    }
  });
};

haste.prototype.configureButtons = function() {
  var _this = this;
  this.buttons = [
    {
      $where: $('#box2 .save'),
      label: 'Save',
      shortcutDescription: 'control + s',
      shortcut: function(evt) {
        return evt.ctrlKey && (evt.keyCode === 83);
      },
      action: function() {
        if (_this.$textarea.val().replace(/^\s+|\s+$/g, '') !== '') {
          _this.lockDocument();
        }
      }
    },
    {
      $where: $('#box2 .new'),
      label: 'New',
      shortcut: function(evt) {
        return evt.ctrlKey && evt.keyCode === 78
      },
      shortcutDescription: 'control + n',
      action: function() {
        _this.newDocument(!_this.doc.key);
      }
    },
    {
      $where: $('#box2 .duplicate'),
      label: 'Duplicate & Edit',
      shortcut: function(evt) {
        return _this.doc.locked && evt.ctrlKey && evt.keyCode === 68;
      },
      shortcutDescription: 'control + d',
      action: function() {
        _this.duplicateDocument();
      }
    },
    {
      $where: $('#box2 .raw'),
      label: 'Just Text',
      shortcut: function(evt) {
        return evt.ctrlKey && evt.shiftKey && evt.keyCode === 82;
      },
      shortcutDescription: 'control + shift + r',
      action: function() {
        window.location.href = '/raw/' + _this.doc.key;
      }
    },
    {
      $where: $('#box2 .twitter'),
      label: 'Twitter',
      shortcut: function(evt) {
        return _this.options.twitter && _this.doc.locked && evt.shiftKey && evt.ctrlKey && evt.keyCode == 84;
      },
      shortcutDescription: 'control + shift + t',
      action: function() {
        window.open('https://twitter.com/share?url=' + encodeURI(window.location.href));
      }
    }
  ];
  for (var i = 0; i < this.buttons.length; i++) {
    this.configureButton(this.buttons[i]);
  }
};

haste.prototype.configureButton = function(options) {
  // Handle the click action
  options.$where.click(function(evt) {
    evt.preventDefault();
    if (!options.clickDisabled && $(this).hasClass('enabled')) {
      options.action();
    }
  });
  // Show the label
  options.$where.mouseenter(function(evt) {
    $('#box3 .label').text(options.label);
    $('#box3 .shortcut').text(options.shortcutDescription || '');
    $('#box3').show();
    $(this).append($('#pointer').remove().show());
  });
  // Hide the label
  options.$where.mouseleave(function(evt) {
    $('#box3').hide();
    $('#pointer').hide();
  });
};

// Configure keyboard shortcuts for the textarea
haste.prototype.configureShortcuts = function() {
  var _this = this;
  $(document.body).keydown(function(evt) {
    var button;
    for (var i = 0 ; i < _this.buttons.length; i++) {
      button = _this.buttons[i];
      if (button.shortcut && button.shortcut(evt)) {
        evt.preventDefault();
        button.action();
        return;
      }
    }
  });
};

///// Tab behavior in the textarea - 2 spaces per tab
$(function() {

  $('textarea').keydown(function(evt) {
    if (evt.keyCode === 9) {
      evt.preventDefault();
      var myValue = '  ';
      // http://stackoverflow.com/questions/946534/insert-text-into-textarea-with-jquery
      // For browsers like Internet Explorer
      if (document.selection) {
        this.focus();
        sel = document.selection.createRange();
        sel.text = myValue;
        this.focus();
      }
      // Mozilla and Webkit
      else if (this.selectionStart || this.selectionStart == '0') {
        var startPos = this.selectionStart;
        var endPos = this.selectionEnd;
        var scrollTop = this.scrollTop;
        this.value = this.value.substring(0, startPos) + myValue +
          this.value.substring(endPos,this.value.length);
        this.focus();
        this.selectionStart = startPos + myValue.length;
        this.selectionEnd = startPos + myValue.length;
        this.scrollTop = scrollTop;
      }
      else {
        this.value += myValue;
        this.focus();
      }
    }
  });

});
<|endoftext|>


<commit_before>/* eslint-disable no-console */

// System documented in https://zulip.readthedocs.io/en/latest/subsystems/logging.html

// This must be included before the first call to $(document).ready
// in order to be able to report exceptions that occur during their
// execution.

import $ from "jquery";

import * as blueslip_stacktrace from "./blueslip_stacktrace";
import {page_params} from "./page_params";
import * as ui_report from "./ui_report";

if (Error.stackTraceLimit !== undefined) {
    Error.stackTraceLimit = 100000;
}

function pad(num, width) {
    return num.toString().padStart(width, "0");
}

function make_logger_func(name) {
    return function Logger_func(...args) {
        const now = new Date();
        const date_str =
            now.getUTCFullYear() +
            "-" +
            pad(now.getUTCMonth() + 1, 2) +
            "-" +
            pad(now.getUTCDate(), 2) +
            " " +
            pad(now.getUTCHours(), 2) +
            ":" +
            pad(now.getUTCMinutes(), 2) +
            ":" +
            pad(now.getUTCSeconds(), 2) +
            "." +
            pad(now.getUTCMilliseconds(), 3) +
            " UTC";

        const str_args = args.map((x) => (typeof x === "object" ? JSON.stringify(x) : x));

        const log_entry = date_str + " " + name.toUpperCase() + ": " + str_args.join("");
        this._memory_log.push(log_entry);

        // Don't let the log grow without bound
        if (this._memory_log.length > 1000) {
            this._memory_log.shift();
        }

        if (console[name] !== undefined) {
            return console[name](...args);
        }
    };
}

class Logger {
    _memory_log = [];

    get_log() {
        return this._memory_log;
    }
}

for (const name of ["debug", "log", "info", "warn", "error"]) {
    Logger.prototype[name] = make_logger_func(name);
}

const logger = new Logger();

export function get_log() {
    return logger.get_log();
}

const reported_errors = new Set();
const last_report_attempt = new Map();

function report_error(
    msg,
    stack = "No stacktrace available",
    {show_ui_msg = false, more_info} = {},
) {
    if (page_params.development_environment) {
        // In development, we display blueslip errors in the web UI,
        // to make them hard to miss.
        blueslip_stacktrace.display_stacktrace(msg, stack);
    }

    const key = ":" + msg + stack;
    if (
        reported_errors.has(key) ||
        (last_report_attempt.has(key) &&
            // Only try to report a given error once every 5 minutes
            Date.now() - last_report_attempt.get(key) <= 60 * 5 * 1000)
    ) {
        return;
    }

    last_report_attempt.set(key, Date.now());

    // TODO: If an exception gets thrown before we set up ajax calls
    // to include the CSRF token, our ajax call will fail.  The
    // elegant thing to do in that case is to either wait until that
    // setup is done or do it ourselves and then retry.
    //
    // Important: We don't use channel.js here so that exceptions
    // always make it to the server even if reload_state.is_in_progress.
    $.ajax({
        type: "POST",
        url: "/json/report/error",
        dataType: "json",
        data: {
            message: msg,
            stacktrace: stack,
            ui_message: show_ui_msg,
            more_info: JSON.stringify(more_info),
            href: window.location.href,
            user_agent: window.navigator.userAgent,
            log: logger.get_log().join("\n"),
        },
        timeout: 3 * 1000,
        success() {
            reported_errors.add(key);
            if (show_ui_msg && ui_report !== undefined) {
                // There are a few races here (and below in the error
                // callback):
                // 1) The ui_report module or something it requires might
                //    not have been compiled or initialized yet.
                // 2) The DOM might not be ready yet and so fetching
                //    the #home-error div might fail.

                // For (1) we just don't show the message if the ui
                // hasn't been loaded yet.  The user will probably
                // get another error once it does.  We can't solve
                // (2) by using $(document).ready because the
                // callback never gets called (I think what's going
                // on here is if the exception was raised by a
                // function that was called as a result of the DOM
                // becoming ready then the internal state of jQuery
                // gets messed up and our callback never gets
                // invoked).  In any case, it will pretty clear that
                // something is wrong with the page and the user will
                // probably try to reload anyway.
                ui_report.client_error(
                    "Oops.  It seems something has gone wrong. " +
                        "The error has been reported to the fine " +
                        "folks at Zulip, but, in the mean time, " +
                        "please try reloading the page.",
                    $("#home-error"),
                );
            }
        },
        error() {
            if (show_ui_msg && ui_report !== undefined) {
                ui_report.client_error(
                    "Oops.  It seems something has gone wrong. Please try reloading the page.",
                    $("#home-error"),
                );
            }
        },
    });

    if (page_params.save_stacktraces) {
        // Save the stacktrace so it can be examined even in
        // development servers.  (N.B. This assumes you have set DEBUG
        // = False on your development server, or else this code path
        // won't execute to begin with -- useful for testing
        // (un)minification.)
        window.last_stacktrace = stack;
    }
}

class BlueslipError extends Error {
    name = "BlueslipError";

    constructor(msg, more_info) {
        super(msg);
        if (more_info !== undefined) {
            this.more_info = more_info;
        }
    }
}

export function exception_msg(ex) {
    let message = ex.message;
    if (ex.fileName !== undefined) {
        message += " at " + ex.fileName;
        if (ex.lineNumber !== undefined) {
            message += ":" + ex.lineNumber;
        }
    }
    return message;
}

$(window).on("error", (event) => {
    const ex = event.originalEvent.error;
    if (!ex || ex instanceof BlueslipError) {
        return;
    }
    const message = exception_msg(ex);
    report_error(message, ex.stack);
});

function build_arg_list(msg, more_info) {
    const args = [msg];
    if (more_info !== undefined) {
        args.push("\nAdditional information: ", more_info);
    }
    return args;
}

export function debug(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.debug(...args);
}

export function log(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.log(...args);
}

export function info(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.info(...args);
}

export function warn(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.warn(...args);
    if (page_params.development_environment) {
        console.trace();
    }
}

export function error(msg, more_info, stack = new Error("dummy").stack) {
    const args = build_arg_list(msg, more_info);
    logger.error(...args);
    report_error(msg, stack, {more_info});

    if (page_params.development_environment) {
        throw new BlueslipError(msg, more_info);
    }

    // This function returns to its caller in production!  To raise a
    // fatal error even in production, use throw new Error(…) instead.
}

export const timings = new Map();

export function measure_time(label, f) {
    const t1 = performance.now();
    const ret = f();
    const t2 = performance.now();
    const elapsed = t2 - t1;
    timings.set(label, elapsed);
    return ret;
}

// Produces an easy-to-read preview on an HTML element.  Currently
// only used for including in error report emails; be sure to discuss
// with other developers before using it in a user-facing context
// because it is not XSS-safe.
export function preview_node(node) {
    if (node instanceof $) {
        node = node[0];
    }

    const tag = node.tagName.toLowerCase();
    const className = node.className.length ? node.className : false;
    const id = node.id.length ? node.id : false;

    const node_preview =
        "<" +
        tag +
        (id ? " id='" + id + "'" : "") +
        (className ? " class='" + className + "'" : "") +
        "></" +
        tag +
        ">";

    return node_preview;
}
<commit_msg>blueslip: Declare log properties directly on the Logger class.<commit_after>/* eslint-disable no-console */

// System documented in https://zulip.readthedocs.io/en/latest/subsystems/logging.html

// This must be included before the first call to $(document).ready
// in order to be able to report exceptions that occur during their
// execution.

import $ from "jquery";

import * as blueslip_stacktrace from "./blueslip_stacktrace";
import {page_params} from "./page_params";
import * as ui_report from "./ui_report";

if (Error.stackTraceLimit !== undefined) {
    Error.stackTraceLimit = 100000;
}

function pad(num, width) {
    return num.toString().padStart(width, "0");
}

function make_logger_func(name) {
    return function Logger_func(...args) {
        const now = new Date();
        const date_str =
            now.getUTCFullYear() +
            "-" +
            pad(now.getUTCMonth() + 1, 2) +
            "-" +
            pad(now.getUTCDate(), 2) +
            " " +
            pad(now.getUTCHours(), 2) +
            ":" +
            pad(now.getUTCMinutes(), 2) +
            ":" +
            pad(now.getUTCSeconds(), 2) +
            "." +
            pad(now.getUTCMilliseconds(), 3) +
            " UTC";

        const str_args = args.map((x) => (typeof x === "object" ? JSON.stringify(x) : x));

        const log_entry = date_str + " " + name.toUpperCase() + ": " + str_args.join("");
        this._memory_log.push(log_entry);

        // Don't let the log grow without bound
        if (this._memory_log.length > 1000) {
            this._memory_log.shift();
        }

        if (console[name] !== undefined) {
            return console[name](...args);
        }
    };
}

class Logger {
    debug = make_logger_func("debug");
    log = make_logger_func("log");
    info = make_logger_func("info");
    warn = make_logger_func("warn");
    error = make_logger_func("error");

    _memory_log = [];

    get_log() {
        return this._memory_log;
    }
}

const logger = new Logger();

export function get_log() {
    return logger.get_log();
}

const reported_errors = new Set();
const last_report_attempt = new Map();

function report_error(
    msg,
    stack = "No stacktrace available",
    {show_ui_msg = false, more_info} = {},
) {
    if (page_params.development_environment) {
        // In development, we display blueslip errors in the web UI,
        // to make them hard to miss.
        blueslip_stacktrace.display_stacktrace(msg, stack);
    }

    const key = ":" + msg + stack;
    if (
        reported_errors.has(key) ||
        (last_report_attempt.has(key) &&
            // Only try to report a given error once every 5 minutes
            Date.now() - last_report_attempt.get(key) <= 60 * 5 * 1000)
    ) {
        return;
    }

    last_report_attempt.set(key, Date.now());

    // TODO: If an exception gets thrown before we set up ajax calls
    // to include the CSRF token, our ajax call will fail.  The
    // elegant thing to do in that case is to either wait until that
    // setup is done or do it ourselves and then retry.
    //
    // Important: We don't use channel.js here so that exceptions
    // always make it to the server even if reload_state.is_in_progress.
    $.ajax({
        type: "POST",
        url: "/json/report/error",
        dataType: "json",
        data: {
            message: msg,
            stacktrace: stack,
            ui_message: show_ui_msg,
            more_info: JSON.stringify(more_info),
            href: window.location.href,
            user_agent: window.navigator.userAgent,
            log: logger.get_log().join("\n"),
        },
        timeout: 3 * 1000,
        success() {
            reported_errors.add(key);
            if (show_ui_msg && ui_report !== undefined) {
                // There are a few races here (and below in the error
                // callback):
                // 1) The ui_report module or something it requires might
                //    not have been compiled or initialized yet.
                // 2) The DOM might not be ready yet and so fetching
                //    the #home-error div might fail.

                // For (1) we just don't show the message if the ui
                // hasn't been loaded yet.  The user will probably
                // get another error once it does.  We can't solve
                // (2) by using $(document).ready because the
                // callback never gets called (I think what's going
                // on here is if the exception was raised by a
                // function that was called as a result of the DOM
                // becoming ready then the internal state of jQuery
                // gets messed up and our callback never gets
                // invoked).  In any case, it will pretty clear that
                // something is wrong with the page and the user will
                // probably try to reload anyway.
                ui_report.client_error(
                    "Oops.  It seems something has gone wrong. " +
                        "The error has been reported to the fine " +
                        "folks at Zulip, but, in the mean time, " +
                        "please try reloading the page.",
                    $("#home-error"),
                );
            }
        },
        error() {
            if (show_ui_msg && ui_report !== undefined) {
                ui_report.client_error(
                    "Oops.  It seems something has gone wrong. Please try reloading the page.",
                    $("#home-error"),
                );
            }
        },
    });

    if (page_params.save_stacktraces) {
        // Save the stacktrace so it can be examined even in
        // development servers.  (N.B. This assumes you have set DEBUG
        // = False on your development server, or else this code path
        // won't execute to begin with -- useful for testing
        // (un)minification.)
        window.last_stacktrace = stack;
    }
}

class BlueslipError extends Error {
    name = "BlueslipError";

    constructor(msg, more_info) {
        super(msg);
        if (more_info !== undefined) {
            this.more_info = more_info;
        }
    }
}

export function exception_msg(ex) {
    let message = ex.message;
    if (ex.fileName !== undefined) {
        message += " at " + ex.fileName;
        if (ex.lineNumber !== undefined) {
            message += ":" + ex.lineNumber;
        }
    }
    return message;
}

$(window).on("error", (event) => {
    const ex = event.originalEvent.error;
    if (!ex || ex instanceof BlueslipError) {
        return;
    }
    const message = exception_msg(ex);
    report_error(message, ex.stack);
});

function build_arg_list(msg, more_info) {
    const args = [msg];
    if (more_info !== undefined) {
        args.push("\nAdditional information: ", more_info);
    }
    return args;
}

export function debug(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.debug(...args);
}

export function log(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.log(...args);
}

export function info(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.info(...args);
}

export function warn(msg, more_info) {
    const args = build_arg_list(msg, more_info);
    logger.warn(...args);
    if (page_params.development_environment) {
        console.trace();
    }
}

export function error(msg, more_info, stack = new Error("dummy").stack) {
    const args = build_arg_list(msg, more_info);
    logger.error(...args);
    report_error(msg, stack, {more_info});

    if (page_params.development_environment) {
        throw new BlueslipError(msg, more_info);
    }

    // This function returns to its caller in production!  To raise a
    // fatal error even in production, use throw new Error(…) instead.
}

export const timings = new Map();

export function measure_time(label, f) {
    const t1 = performance.now();
    const ret = f();
    const t2 = performance.now();
    const elapsed = t2 - t1;
    timings.set(label, elapsed);
    return ret;
}

// Produces an easy-to-read preview on an HTML element.  Currently
// only used for including in error report emails; be sure to discuss
// with other developers before using it in a user-facing context
// because it is not XSS-safe.
export function preview_node(node) {
    if (node instanceof $) {
        node = node[0];
    }

    const tag = node.tagName.toLowerCase();
    const className = node.className.length ? node.className : false;
    const id = node.id.length ? node.id : false;

    const node_preview =
        "<" +
        tag +
        (id ? " id='" + id + "'" : "") +
        (className ? " class='" + className + "'" : "") +
        "></" +
        tag +
        ">";

    return node_preview;
}
<|endoftext|>