<commit_before>// Global variables
var FileName = 'credentials';
var RoleArns = {};

// When this background process starts, load variables from chrome storage
// from saved Extension Options
loadItemsFromStorage();
// Additionaly on start of the background process it is checked if this extension can be activated
chrome.storage.sync.get({
    // The default is activated
    Activated: true
  }, function(item) {
    if (item.Activated) addOnBeforeRequestEventListener();
});
// Additionaly on start of the background process it is checked if a new version of the plugin is installed.
// If so, show the user the changelog
// var thisVersion = chrome.runtime.getManifest().version;
chrome.runtime.onInstalled.addListener(function(details){
	if(details.reason == "install" || details.reason == "update"){
		// Open a new tab to show changelog html page
		chrome.tabs.create({url: "../options/changelog.html"});
    }
});

// Function to be called when this extension is activated.
// This adds an EventListener for each request to signin.aws.amazon.com
function addOnBeforeRequestEventListener() {
  if (chrome.webRequest.onBeforeRequest.hasListener(onBeforeRequestEvent)) {
    console.log("ERROR: onBeforeRequest EventListener could not be added, because onBeforeRequest already has an EventListener.");
  } else {
    chrome.webRequest.onBeforeRequest.addListener(
      onBeforeRequestEvent,
      {urls: ["https://signin.aws.amazon.com/saml"]},
      ["requestBody"]
    );
  }
}

// Function to be called when this extension is de-actived
// by unchecking the activation checkbox on the popup page
function removeOnBeforeRequestEventListener() {
  chrome.webRequest.onBeforeRequest.removeListener(onBeforeRequestEvent);
}

// Callback function for the webRequest OnBeforeRequest EventListener
// This function runs on each request to https://signin.aws.amazon.com/saml
function onBeforeRequestEvent(details) {
  // Decode base64 SAML assertion in the request
  var samlXmlDoc = "";
  var formDataPayload = undefined;
  if (details.requestBody.formData) {
    samlXmlDoc = decodeURIComponent(unescape(window.atob(details.requestBody.formData.SAMLResponse[0])));
  } else if (details.requestBody.raw) {
    var combined = new ArrayBuffer(0);
    details.requestBody.raw.forEach(function(element) {
      var tmp = new Uint8Array(combined.byteLength + element.bytes.byteLength);
      tmp.set( new Uint8Array(combined), 0 );
      tmp.set( new Uint8Array(element.bytes),combined.byteLength );
      combined = tmp.buffer;
    });
    var combinedView = new DataView(combined);
    var decoder = new TextDecoder('utf-8');
    formDataPayload = new URLSearchParams(decoder.decode(combinedView));
    samlXmlDoc = decodeURIComponent(unescape(window.atob(formDataPayload.get('SAMLResponse'))))
  }
  // Convert XML String to DOM
  parser = new DOMParser()
  domDoc = parser.parseFromString(samlXmlDoc, "text/xml");
  // Get a list of claims (= AWS roles) from the SAML assertion
  var roleDomNodes = domDoc.querySelectorAll('[Name="https://aws.amazon.com/SAML/Attributes/Role"]')[0].childNodes
  // Parse the PrincipalArn and the RoleArn from the SAML Assertion.
  var PrincipalArn = '';
  var RoleArn = '';
  var SAMLAssertion = undefined;
  var hasRoleIndex = false;
  var roleIndex = "";
  if (details.requestBody.formData) {
    SAMLAssertion = details.requestBody.formData.SAMLResponse[0];
    hasRoleIndex = "roleIndex" in details.requestBody.formData;
    roleIndex = details.requestBody.formData.roleIndex[0];
  } else if (formDataPayload) {
    SAMLAssertion = formDataPayload.get('SAMLResponse');
    roleIndex = formDataPayload.get('roleIndex');
    hasRoleIndex = roleIndex != undefined;
  }
   // If there is more than 1 role in the claim, look at the 'roleIndex' HTTP Form data parameter to determine the role to assume
  if (roleDomNodes.length > 1 && hasRoleIndex) {
    for (i = 0; i < roleDomNodes.length; i++) {
      var nodeValue = roleDomNodes[i].innerHTML;
      if (nodeValue.indexOf(roleIndex) > -1) {
        // This DomNode holdes the data for the role to assume. Use these details for the assumeRoleWithSAML API call
		// The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments.
		extractPrincipalPlusRoleAndAssumeRole(nodeValue, SAMLAssertion)
      }
    }
  }
  // If there is just 1 role in the claim there will be no 'roleIndex' in the form data.
  else if (roleDomNodes.length == 1) {
    // When there is just 1 role in the claim, use these details for the assumeRoleWithSAML API call
	// The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments.
	extractPrincipalPlusRoleAndAssumeRole(roleDomNodes[0].innerHTML, SAMLAssertion)
  }
}

// Called from 'onBeforeRequestEvent' function.
// Gets a Role Attribute from a SAMLAssertion as function argument. Gets the SAMLAssertion as a second argument.
// This function extracts the RoleArn and PrincipalArn (SAML-provider)
// from this argument and uses it to call the AWS STS assumeRoleWithSAML API.
function extractPrincipalPlusRoleAndAssumeRole(samlattribute, SAMLAssertion) {
	// Pattern for Role
	var reRole = /arn:aws:iam:[^:]*:[0-9]+:role\/[^,]+/i;
	// Patern for Principal (SAML Provider)
	var rePrincipal = /arn:aws:iam:[^:]*:[0-9]+:saml-provider\/[^,]+/i;
	// Extraxt both regex patterns from SAMLAssertion attribute
	RoleArn = samlattribute.match(reRole)[0];
	PrincipalArn = samlattribute.match(rePrincipal)[0];

	// Set parameters needed for assumeRoleWithSAML method
	var params = {
		PrincipalArn: PrincipalArn,
		RoleArn: RoleArn,
		SAMLAssertion: SAMLAssertion,
	};
	// Call STS API from AWS
	var sts = new AWS.STS();
	sts.assumeRoleWithSAML(params, function(err, data) {
		if (err) console.log(err, err.stack); // an error occurred
		else {
			// On succesful API response create file with the STS keys
			var docContent = "[default] \n" +
			"aws_access_key_id = " + data.Credentials.AccessKeyId + " \n" +
			"aws_secret_access_key = " + data.Credentials.SecretAccessKey + " \n" +
			"aws_session_token = " + data.Credentials.SessionToken;

			// If there are no Role ARNs configured in the options panel, continue to create credentials file
			// Otherwise, extend docContent with a profile for each specified ARN in the options panel
			if (Object.keys(RoleArns).length == 0) {
				console.log('Output maken');
				outputDocAsDownload(docContent);
			} else {
				var profileList = Object.keys(RoleArns);
				console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[0]]);
				assumeAdditionalRole(profileList, 0, data.Credentials.AccessKeyId, data.Credentials.SecretAccessKey, data.Credentials.SessionToken, docContent);
			}
		}
	});
}

// Will fetch additional STS keys for 1 role from the RoleArns dict
// The assume-role API is called using the credentials (STS keys) fetched using the SAML claim. Basically the default profile.
function assumeAdditionalRole(profileList, index, AccessKeyId, SecretAccessKey, SessionToken, docContent) {
	// Set the fetched STS keys from the SAML reponse as credentials for doing the API call
	var options = {'accessKeyId': AccessKeyId, 'secretAccessKey': SecretAccessKey, 'sessionToken': SessionToken};
	var sts = new AWS.STS(options);
	// Set the parameters for the AssumeRole API call. Meaning: What role to assume
	var params = {
		RoleArn: RoleArns[profileList[index]],
		RoleSessionName: profileList[index]
	};
	// Call the API
	sts.assumeRole(params, function(err, data) {
		if (err) console.log(err, err.stack); // an error occurred
		else {
			docContent += " \n\n" +
			"[" + profileList[index] + "] \n" +
			"aws_access_key_id = " + data.Credentials.AccessKeyId + " \n" +
			"aws_secret_access_key = " + data.Credentials.SecretAccessKey + " \n" +
			"aws_session_token = " + data.Credentials.SessionToken;
		}
		// If there are more profiles/roles in the RoleArns dict, do another call of assumeAdditionalRole to extend the docContent with another profile
		// Otherwise, this is the last profile/role in the RoleArns dict. Proceed to creating the credentials file
		if (index < profileList.length - 1) {
			console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[index + 1]]);
			assumeAdditionalRole(profileList, index + 1, AccessKeyId, SecretAccessKey, SessionToken, docContent);
		} else {
			outputDocAsDownload(docContent);
		}
	});
}

// Called from either extractPrincipalPlusRoleAndAssumeRole (if RoleArns dict is empty)
// Otherwise called from assumeAdditionalRole as soon as all roles from RoleArns have been assumed
function outputDocAsDownload(docContent) {
	var doc = URL.createObjectURL( new Blob([docContent], {type: 'application/octet-binary'}) );
	// Triggers download of the generated file
	chrome.downloads.download({ url: doc, filename: FileName, conflictAction: 'overwrite', saveAs: false });
}

// This Listener receives messages from options.js and popup.js
// Received messages are meant to affect the background process.
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    // When the options are changed in the Options panel
    // these items need to be reloaded in this background process.
    if (request.action == "reloadStorageItems") {
      loadItemsFromStorage();
      sendResponse({message: "Storage items reloaded in background process."});
    }
    // When the activation checkbox on the popup screen is checked/unchecked
    // the webRequest event listener needs to be added or removed.
    if (request.action == "addWebRequestEventListener") {
      addOnBeforeRequestEventListener();
      sendResponse({message: "webRequest EventListener added in background process."});
    }
    if (request.action == "removeWebRequestEventListener") {
      removeOnBeforeRequestEventListener();
      sendResponse({message: "webRequest EventListener removed in background process."});
    }
  });

function loadItemsFromStorage() {
  chrome.storage.sync.get({
    FileName: 'credentials',
	RoleArns: {}
  }, function(items) {
    FileName = items.FileName;
	RoleArns = items.RoleArns;
  });
}
<commit_msg>Support SessionDuration if present in SAML Response<commit_after>// Global variables
var FileName = 'credentials';
var RoleArns = {};

// When this background process starts, load variables from chrome storage
// from saved Extension Options
loadItemsFromStorage();
// Additionaly on start of the background process it is checked if this extension can be activated
chrome.storage.sync.get({
    // The default is activated
    Activated: true
  }, function(item) {
    if (item.Activated) addOnBeforeRequestEventListener();
});
// Additionaly on start of the background process it is checked if a new version of the plugin is installed.
// If so, show the user the changelog
// var thisVersion = chrome.runtime.getManifest().version;
chrome.runtime.onInstalled.addListener(function(details){
	if(details.reason == "install" || details.reason == "update"){
		// Open a new tab to show changelog html page
		chrome.tabs.create({url: "../options/changelog.html"});
    }
});

// Function to be called when this extension is activated.
// This adds an EventListener for each request to signin.aws.amazon.com
function addOnBeforeRequestEventListener() {
  if (chrome.webRequest.onBeforeRequest.hasListener(onBeforeRequestEvent)) {
    console.log("ERROR: onBeforeRequest EventListener could not be added, because onBeforeRequest already has an EventListener.");
  } else {
    chrome.webRequest.onBeforeRequest.addListener(
      onBeforeRequestEvent,
      {urls: ["https://signin.aws.amazon.com/saml"]},
      ["requestBody"]
    );
  }
}

// Function to be called when this extension is de-actived
// by unchecking the activation checkbox on the popup page
function removeOnBeforeRequestEventListener() {
  chrome.webRequest.onBeforeRequest.removeListener(onBeforeRequestEvent);
}

// Callback function for the webRequest OnBeforeRequest EventListener
// This function runs on each request to https://signin.aws.amazon.com/saml
function onBeforeRequestEvent(details) {
  // Decode base64 SAML assertion in the request
  var samlXmlDoc = "";
  var formDataPayload = undefined;
  if (details.requestBody.formData) {
    samlXmlDoc = decodeURIComponent(unescape(window.atob(details.requestBody.formData.SAMLResponse[0])));
  } else if (details.requestBody.raw) {
    var combined = new ArrayBuffer(0);
    details.requestBody.raw.forEach(function(element) {
      var tmp = new Uint8Array(combined.byteLength + element.bytes.byteLength);
      tmp.set( new Uint8Array(combined), 0 );
      tmp.set( new Uint8Array(element.bytes),combined.byteLength );
      combined = tmp.buffer;
    });
    var combinedView = new DataView(combined);
    var decoder = new TextDecoder('utf-8');
    formDataPayload = new URLSearchParams(decoder.decode(combinedView));
    samlXmlDoc = decodeURIComponent(unescape(window.atob(formDataPayload.get('SAMLResponse'))))
  }
  // Convert XML String to DOM
  parser = new DOMParser()
  domDoc = parser.parseFromString(samlXmlDoc, "text/xml");
  // Get a list of claims (= AWS roles) from the SAML assertion
  var roleDomNodes = domDoc.querySelectorAll('[Name="https://aws.amazon.com/SAML/Attributes/Role"]')[0].childNodes
  // Parse the PrincipalArn and the RoleArn from the SAML Assertion.
  var PrincipalArn = '';
  var RoleArn = '';
  var SAMLAssertion = undefined;
  var SessionDuration = domDoc.querySelectorAll('[Name="https://aws.amazon.com/SAML/Attributes/SessionDuration"]')[0]
  var hasRoleIndex = false;
  var roleIndex = "";
  if (details.requestBody.formData) {
    SAMLAssertion = details.requestBody.formData.SAMLResponse[0];
    hasRoleIndex = "roleIndex" in details.requestBody.formData;
    roleIndex = details.requestBody.formData.roleIndex[0];
  } else if (formDataPayload) {
    SAMLAssertion = formDataPayload.get('SAMLResponse');
    roleIndex = formDataPayload.get('roleIndex');
    hasRoleIndex = roleIndex != undefined;
  }

  if (SessionDuration !== undefined) {
    SessionDuration = Number(SessionDuration.firstElementChild.textContent)
  } else {
    // Amazon default
    SessionDuration = 3600;
  }

   // If there is more than 1 role in the claim, look at the 'roleIndex' HTTP Form data parameter to determine the role to assume
  if (roleDomNodes.length > 1 && hasRoleIndex) {
    for (i = 0; i < roleDomNodes.length; i++) {
      var nodeValue = roleDomNodes[i].innerHTML;
      if (nodeValue.indexOf(roleIndex) > -1) {
        // This DomNode holdes the data for the role to assume. Use these details for the assumeRoleWithSAML API call
		// The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments.
		extractPrincipalPlusRoleAndAssumeRole(nodeValue, SAMLAssertion, SessionDuration)
      }
    }
  }
  // If there is just 1 role in the claim there will be no 'roleIndex' in the form data.
  else if (roleDomNodes.length == 1) {
    // When there is just 1 role in the claim, use these details for the assumeRoleWithSAML API call
	// The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments.
	extractPrincipalPlusRoleAndAssumeRole(roleDomNodes[0].innerHTML, SAMLAssertion, SessionDuration)
  }
}

// Called from 'onBeforeRequestEvent' function.
// Gets a Role Attribute from a SAMLAssertion as function argument. Gets the SAMLAssertion as a second argument.
// This function extracts the RoleArn and PrincipalArn (SAML-provider)
// from this argument and uses it to call the AWS STS assumeRoleWithSAML API.
function extractPrincipalPlusRoleAndAssumeRole(samlattribute, SAMLAssertion, SessionDuration) {
	// Pattern for Role
	var reRole = /arn:aws:iam:[^:]*:[0-9]+:role\/[^,]+/i;
	// Patern for Principal (SAML Provider)
	var rePrincipal = /arn:aws:iam:[^:]*:[0-9]+:saml-provider\/[^,]+/i;
	// Extraxt both regex patterns from SAMLAssertion attribute
	RoleArn = samlattribute.match(reRole)[0];
	PrincipalArn = samlattribute.match(rePrincipal)[0];

	// Set parameters needed for assumeRoleWithSAML method
	var params = {
		PrincipalArn: PrincipalArn,
		RoleArn: RoleArn,
		SAMLAssertion: SAMLAssertion,
		DurationSeconds: SessionDuration,
	};
	// Call STS API from AWS
	var sts = new AWS.STS();
	sts.assumeRoleWithSAML(params, function(err, data) {
		if (err) console.log(err, err.stack); // an error occurred
		else {
			// On succesful API response create file with the STS keys
			var docContent = "[default] \n" +
			"aws_access_key_id = " + data.Credentials.AccessKeyId + " \n" +
			"aws_secret_access_key = " + data.Credentials.SecretAccessKey + " \n" +
			"aws_session_token = " + data.Credentials.SessionToken;

			// If there are no Role ARNs configured in the options panel, continue to create credentials file
			// Otherwise, extend docContent with a profile for each specified ARN in the options panel
			if (Object.keys(RoleArns).length == 0) {
				console.log('Output maken');
				outputDocAsDownload(docContent);
			} else {
				var profileList = Object.keys(RoleArns);
				console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[0]]);
				assumeAdditionalRole(profileList, 0, data.Credentials.AccessKeyId, data.Credentials.SecretAccessKey, data.Credentials.SessionToken, docContent, SessionDuration);
			}
		}
	});
}

// Will fetch additional STS keys for 1 role from the RoleArns dict
// The assume-role API is called using the credentials (STS keys) fetched using the SAML claim. Basically the default profile.
function assumeAdditionalRole(profileList, index, AccessKeyId, SecretAccessKey, SessionToken, docContent, SessionDuration) {
	// Set the fetched STS keys from the SAML reponse as credentials for doing the API call
	var options = {'accessKeyId': AccessKeyId, 'secretAccessKey': SecretAccessKey, 'sessionToken': SessionToken};
	var sts = new AWS.STS(options);
	// Set the parameters for the AssumeRole API call. Meaning: What role to assume
	var params = {
		RoleArn: RoleArns[profileList[index]],
		RoleSessionName: profileList[index],
		DurationSeconds: SessionDuration,
	};
	// Call the API
	sts.assumeRole(params, function(err, data) {
		if (err) console.log(err, err.stack); // an error occurred
		else {
			docContent += " \n\n" +
			"[" + profileList[index] + "] \n" +
			"aws_access_key_id = " + data.Credentials.AccessKeyId + " \n" +
			"aws_secret_access_key = " + data.Credentials.SecretAccessKey + " \n" +
			"aws_session_token = " + data.Credentials.SessionToken;
		}
		// If there are more profiles/roles in the RoleArns dict, do another call of assumeAdditionalRole to extend the docContent with another profile
		// Otherwise, this is the last profile/role in the RoleArns dict. Proceed to creating the credentials file
		if (index < profileList.length - 1) {
			console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[index + 1]]);
			assumeAdditionalRole(profileList, index + 1, AccessKeyId, SecretAccessKey, SessionToken, docContent);
		} else {
			outputDocAsDownload(docContent);
		}
	});
}

// Called from either extractPrincipalPlusRoleAndAssumeRole (if RoleArns dict is empty)
// Otherwise called from assumeAdditionalRole as soon as all roles from RoleArns have been assumed
function outputDocAsDownload(docContent) {
	var doc = URL.createObjectURL( new Blob([docContent], {type: 'application/octet-binary'}) );
	// Triggers download of the generated file
	chrome.downloads.download({ url: doc, filename: FileName, conflictAction: 'overwrite', saveAs: false });
}

// This Listener receives messages from options.js and popup.js
// Received messages are meant to affect the background process.
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    // When the options are changed in the Options panel
    // these items need to be reloaded in this background process.
    if (request.action == "reloadStorageItems") {
      loadItemsFromStorage();
      sendResponse({message: "Storage items reloaded in background process."});
    }
    // When the activation checkbox on the popup screen is checked/unchecked
    // the webRequest event listener needs to be added or removed.
    if (request.action == "addWebRequestEventListener") {
      addOnBeforeRequestEventListener();
      sendResponse({message: "webRequest EventListener added in background process."});
    }
    if (request.action == "removeWebRequestEventListener") {
      removeOnBeforeRequestEventListener();
      sendResponse({message: "webRequest EventListener removed in background process."});
    }
  });

function loadItemsFromStorage() {
  chrome.storage.sync.get({
    FileName: 'credentials',
	RoleArns: {}
  }, function(items) {
    FileName = items.FileName;
	RoleArns = items.RoleArns;
  });
}
<|endoftext|>


<commit_before>(browser => {
  if (!('fragmentDirective' in window.location)) {
    return;
  }

  // https://wicg.github.io/ScrollToTextFragment/#:~:text=It%20is%20recommended,a%20range%2Dbased%20match.
  // Experimenting with 100 instead.
  EXACT_MATCH_MAX_LENGTH = 100;
  INNER_CONTEXT_MAX_LENGTH = 2;
  DEBUG = true;

  const log = (...args) => {
    if (DEBUG) {
      console.log.apply(this, args);
    }
  };

  browser.contextMenus.create({
    title: chrome.i18n.getMessage('copy_link'),
    id: 'copy-link',
    contexts: ['all']
  });

  browser.contextMenus.onClicked.addListener(async (info, tab) => {
    const selectedText = info.selectionText;
    if (!selectedText) {
      return;
    }
    const textFragmentURL = await createURL(tab.url);
    await copyToClipboard(textFragmentURL);
  });

  const escapeRegExp = s => {
    return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  };

  const unescapeRegExp = s => {
    return s.replace(/\\([-\/\\^$*+?.()|[\]{}])/g, '$1');
  };

  const isUniqueMatch = (hayStack, textStart, textEnd = '') => {
    try {
      const needle = new RegExp(`${textStart}${textEnd}`, 'gims');
      const matches = [...hayStack.matchAll(needle)];
      log('———\n', 'RegEx:', needle.source, '\n', 'Matches:', matches, '\n———');
      return matches;
    } catch (err) {
      console.error(err.name, err.message);
      return [];
    }
  };

  const findUniqueMatch = (
    pageText,
    textStart,
    textEnd,
    matches,
    wordsBefore,
    wordsAfter,
    growContextBefore,
    before = '',
    after = ''
  ) => {
    log(
      ' before: "' + before + '"\n',
      'textStart: "' + textStart + '"\n',
      'textEnd: "' + textEnd + '"\n',
      'after: "' + after + '"\n',
      'growContextBefore: ' + growContextBefore
    );
    // We need to add context before or after the needle.
    // Throw a coin to decide each time whether before or after.
    if (growContextBefore && wordsBefore.length > 0) {
      const newBefore = escapeRegExp(wordsBefore.pop());
      before = `${newBefore}${before ? ` ${before}` : ''}`;
      log('new before "' + before + '"');
    } else if (wordsAfter.length > 0) {
      const newAfter = escapeRegExp(wordsAfter.shift());
      after = `${after ? `${after} ` : ''}${newAfter}`;
      log('new after "' + after + '"');
    }
    matches = isUniqueMatch(
      pageText,
      `${before ? `${before}.?` : ''}${textStart}`,
      `${textEnd ? `.*?${textEnd}` : ''}${after ? `.?${after}` : ''}`
    );
    if (matches.length === 1) {
      return {
        before: unescapeRegExp(before),
        after: unescapeRegExp(after)
      };
    } else if (
      matches.length === 0 ||
      (wordsBefore.length === 0 && wordsAfter.length === 0)
    ) {
      // This should never happen, but, hey… ¯\_(ツ)_/¯
      return {
        before: false,
        after: false
      };
    }
    return findUniqueMatch(
      pageText,
      textStart,
      textEnd,
      matches,
      wordsBefore,
      wordsAfter,
      growContextBefore,
      before,
      after
    );
  };

  const encodeURIComponentAndMinus = text => {
    return encodeURIComponent(text).replace(/-/g, '%2D');
  };

  const createURL = async tabURL => {
    const {
      selectedText,
      pageText,
      textBeforeSelection,
      textAfterSelection,
      textNodeBeforeSelection,
      textNodeAfterSelection,
      closestElementFragment
    } = await getPageTextContent();
    tabURL = new URL(tabURL);
    let textFragmentURL = `${tabURL.origin}${tabURL.pathname}${
      closestElementFragment ? `#${closestElementFragment}` : '#'
    }`;
    const selectedWords = selectedText.split(/\s+/gm);
    let textStart = '';
    let textEnd = '';
    if (
      selectedWords.length === 1 ||
      selectedText.length <= EXACT_MATCH_MAX_LENGTH
    ) {
      // Just use the entire text
      textStart = selectedText;
    } else {
      // Use the first and the last word of the selection.
      textStart = selectedWords.shift();
      textEnd = selectedWords.pop();
      // Add inner context at the beginning and the end.
      if (
        selectedText.length > EXACT_MATCH_MAX_LENGTH &&
        INNER_CONTEXT_MAX_LENGTH > 0
      ) {
        textStart +=
          ' ' + selectedWords.splice(0, INNER_CONTEXT_MAX_LENGTH).join(' ');
        textEnd =
          selectedWords.splice(-1 * INNER_CONTEXT_MAX_LENGTH).join(' ') +
          ' ' +
          textEnd;
      }
    }
    let matches = isUniqueMatch(
      pageText,
      escapeRegExp(textStart),
      `${textEnd ? `.*?${escapeRegExp(textEnd)}` : ''}`
    );
    if (matches.length === 1) {
      // We have a unique match, return it.
      textStart = encodeURIComponentAndMinus(textStart);
      textEnd = textEnd ? `,${encodeURIComponentAndMinus(textEnd)}` : '';
      return (textFragmentURL += `:~:text=${textStart}${textEnd}`);
    } else {
      // We need to add context. Therefore, use the text before/after in the
      // same node as the selected text combined with the text in the
      // previous/next node.
      const wordsInTextNodeBeforeSelection = textNodeBeforeSelection
        ? textNodeBeforeSelection.split(/\s+/gm)
        : [];
      const wordsBeforeSelection = textBeforeSelection
        ? textBeforeSelection.split(/\s+/gm)
        : [];
      const wordsBefore = wordsInTextNodeBeforeSelection.concat(
        wordsBeforeSelection
      );

      const wordsInTextNodeAfterSelection = textNodeAfterSelection
        ? textNodeAfterSelection.split(/\s+/gm)
        : [];
      const wordsAfterSelection = textAfterSelection
        ? textAfterSelection.split(/\s+/gmu)
        : [];
      const wordsAfter = wordsAfterSelection.concat(
        wordsInTextNodeAfterSelection
      );
      // Add context either before or after the selected text, depending on
      // where there is more text.
      const growContextBefore =
        wordsBeforeSelection.length > wordsAfterSelection.length;

      let { before, after } = findUniqueMatch(
        pageText,
        textStart,
        textEnd,
        matches,
        wordsBefore,
        wordsAfter,
        growContextBefore
      );
      if (!before && !after) {
        return await sendMessageToPage('failure');
      }
      before = before ? `${encodeURIComponentAndMinus(before)}-,` : '';
      after = after ? `,-${encodeURIComponentAndMinus(after)}` : '';
      textStart = encodeURIComponentAndMinus(textStart);
      textEnd = textEnd ? `,${encodeURIComponentAndMinus(textEnd)}` : '';
      return (textFragmentURL += `:~:text=${before}${textStart}${textEnd}${after}`);
    }
  };

  const getPageTextContent = async () => {
    return new Promise(resolve => {
      browser.tabs.query(
        {
          active: true,
          currentWindow: true
        },
        tabs => {
          browser.tabs.sendMessage(
            tabs[0].id,
            {
              message: 'get-text'
            },
            response => {
              return resolve(response);
            }
          );
        }
      );
    });
  };

  const sendMessageToPage = async (message, data = null) => {
    browser.tabs.query(
      {
        active: true,
        currentWindow: true
      },
      tabs => {
        browser.tabs.sendMessage(tabs[0].id, {
          message,
          data
        });
      }
    );
  };

  const copyToClipboard = async url => {
    try {
      const { state } = await navigator.permissions.query({
        name: 'clipboard-write'
      });
      if (state !== 'granted') {
        throw new Error('Clipboard permission not granted');
      }
      await navigator.clipboard.writeText(url);
    } catch {
      let textArea = document.createElement('textarea');
      document.body.append(textArea);
      textArea.textContent = url;
      textArea.select();
      document.execCommand('copy');
      textArea.remove();
    }
    log(url);
    await sendMessageToPage('success', url);
  };
})(window.chrome || window.browser);
<commit_msg>Remove u flag<commit_after>(browser => {
  if (!('fragmentDirective' in window.location)) {
    return;
  }

  // https://wicg.github.io/ScrollToTextFragment/#:~:text=It%20is%20recommended,a%20range%2Dbased%20match.
  // Experimenting with 100 instead.
  EXACT_MATCH_MAX_LENGTH = 100;
  INNER_CONTEXT_MAX_LENGTH = 2;
  DEBUG = true;

  const log = (...args) => {
    if (DEBUG) {
      console.log.apply(this, args);
    }
  };

  browser.contextMenus.create({
    title: chrome.i18n.getMessage('copy_link'),
    id: 'copy-link',
    contexts: ['all']
  });

  browser.contextMenus.onClicked.addListener(async (info, tab) => {
    const selectedText = info.selectionText;
    if (!selectedText) {
      return;
    }
    const textFragmentURL = await createURL(tab.url);
    await copyToClipboard(textFragmentURL);
  });

  const escapeRegExp = s => {
    return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  };

  const unescapeRegExp = s => {
    return s.replace(/\\([-\/\\^$*+?.()|[\]{}])/g, '$1');
  };

  const isUniqueMatch = (hayStack, textStart, textEnd = '') => {
    try {
      const needle = new RegExp(`${textStart}${textEnd}`, 'gims');
      const matches = [...hayStack.matchAll(needle)];
      log('———\n', 'RegEx:', needle.source, '\n', 'Matches:', matches, '\n———');
      return matches;
    } catch (err) {
      console.error(err.name, err.message);
      return [];
    }
  };

  const findUniqueMatch = (
    pageText,
    textStart,
    textEnd,
    matches,
    wordsBefore,
    wordsAfter,
    growContextBefore,
    before = '',
    after = ''
  ) => {
    log(
      ' before: "' + before + '"\n',
      'textStart: "' + textStart + '"\n',
      'textEnd: "' + textEnd + '"\n',
      'after: "' + after + '"\n',
      'growContextBefore: ' + growContextBefore
    );
    // We need to add context before or after the needle.
    // Throw a coin to decide each time whether before or after.
    if (growContextBefore && wordsBefore.length > 0) {
      const newBefore = escapeRegExp(wordsBefore.pop());
      before = `${newBefore}${before ? ` ${before}` : ''}`;
      log('new before "' + before + '"');
    } else if (wordsAfter.length > 0) {
      const newAfter = escapeRegExp(wordsAfter.shift());
      after = `${after ? `${after} ` : ''}${newAfter}`;
      log('new after "' + after + '"');
    }
    matches = isUniqueMatch(
      pageText,
      `${before ? `${before}.?` : ''}${textStart}`,
      `${textEnd ? `.*?${textEnd}` : ''}${after ? `.?${after}` : ''}`
    );
    if (matches.length === 1) {
      return {
        before: unescapeRegExp(before),
        after: unescapeRegExp(after)
      };
    } else if (
      matches.length === 0 ||
      (wordsBefore.length === 0 && wordsAfter.length === 0)
    ) {
      // This should never happen, but, hey… ¯\_(ツ)_/¯
      return {
        before: false,
        after: false
      };
    }
    return findUniqueMatch(
      pageText,
      textStart,
      textEnd,
      matches,
      wordsBefore,
      wordsAfter,
      growContextBefore,
      before,
      after
    );
  };

  const encodeURIComponentAndMinus = text => {
    return encodeURIComponent(text).replace(/-/g, '%2D');
  };

  const createURL = async tabURL => {
    const {
      selectedText,
      pageText,
      textBeforeSelection,
      textAfterSelection,
      textNodeBeforeSelection,
      textNodeAfterSelection,
      closestElementFragment
    } = await getPageTextContent();
    tabURL = new URL(tabURL);
    let textFragmentURL = `${tabURL.origin}${tabURL.pathname}${
      closestElementFragment ? `#${closestElementFragment}` : '#'
    }`;
    const selectedWords = selectedText.split(/\s+/gm);
    let textStart = '';
    let textEnd = '';
    if (
      selectedWords.length === 1 ||
      selectedText.length <= EXACT_MATCH_MAX_LENGTH
    ) {
      // Just use the entire text
      textStart = selectedText;
    } else {
      // Use the first and the last word of the selection.
      textStart = selectedWords.shift();
      textEnd = selectedWords.pop();
      // Add inner context at the beginning and the end.
      if (
        selectedText.length > EXACT_MATCH_MAX_LENGTH &&
        INNER_CONTEXT_MAX_LENGTH > 0
      ) {
        textStart +=
          ' ' + selectedWords.splice(0, INNER_CONTEXT_MAX_LENGTH).join(' ');
        textEnd =
          selectedWords.splice(-1 * INNER_CONTEXT_MAX_LENGTH).join(' ') +
          ' ' +
          textEnd;
      }
    }
    let matches = isUniqueMatch(
      pageText,
      escapeRegExp(textStart),
      `${textEnd ? `.*?${escapeRegExp(textEnd)}` : ''}`
    );
    if (matches.length === 1) {
      // We have a unique match, return it.
      textStart = encodeURIComponentAndMinus(textStart);
      textEnd = textEnd ? `,${encodeURIComponentAndMinus(textEnd)}` : '';
      return (textFragmentURL += `:~:text=${textStart}${textEnd}`);
    } else {
      // We need to add context. Therefore, use the text before/after in the
      // same node as the selected text combined with the text in the
      // previous/next node.
      const wordsInTextNodeBeforeSelection = textNodeBeforeSelection
        ? textNodeBeforeSelection.split(/\s+/gm)
        : [];
      const wordsBeforeSelection = textBeforeSelection
        ? textBeforeSelection.split(/\s+/gm)
        : [];
      const wordsBefore = wordsInTextNodeBeforeSelection.concat(
        wordsBeforeSelection
      );

      const wordsInTextNodeAfterSelection = textNodeAfterSelection
        ? textNodeAfterSelection.split(/\s+/gm)
        : [];
      const wordsAfterSelection = textAfterSelection
        ? textAfterSelection.split(/\s+/gm)
        : [];
      const wordsAfter = wordsAfterSelection.concat(
        wordsInTextNodeAfterSelection
      );
      // Add context either before or after the selected text, depending on
      // where there is more text.
      const growContextBefore =
        wordsBeforeSelection.length > wordsAfterSelection.length;

      let { before, after } = findUniqueMatch(
        pageText,
        textStart,
        textEnd,
        matches,
        wordsBefore,
        wordsAfter,
        growContextBefore
      );
      if (!before && !after) {
        return await sendMessageToPage('failure');
      }
      before = before ? `${encodeURIComponentAndMinus(before)}-,` : '';
      after = after ? `,-${encodeURIComponentAndMinus(after)}` : '';
      textStart = encodeURIComponentAndMinus(textStart);
      textEnd = textEnd ? `,${encodeURIComponentAndMinus(textEnd)}` : '';
      return (textFragmentURL += `:~:text=${before}${textStart}${textEnd}${after}`);
    }
  };

  const getPageTextContent = async () => {
    return new Promise(resolve => {
      browser.tabs.query(
        {
          active: true,
          currentWindow: true
        },
        tabs => {
          browser.tabs.sendMessage(
            tabs[0].id,
            {
              message: 'get-text'
            },
            response => {
              return resolve(response);
            }
          );
        }
      );
    });
  };

  const sendMessageToPage = async (message, data = null) => {
    browser.tabs.query(
      {
        active: true,
        currentWindow: true
      },
      tabs => {
        browser.tabs.sendMessage(tabs[0].id, {
          message,
          data
        });
      }
    );
  };

  const copyToClipboard = async url => {
    try {
      const { state } = await navigator.permissions.query({
        name: 'clipboard-write'
      });
      if (state !== 'granted') {
        throw new Error('Clipboard permission not granted');
      }
      await navigator.clipboard.writeText(url);
    } catch {
      let textArea = document.createElement('textarea');
      document.body.append(textArea);
      textArea.textContent = url;
      textArea.select();
      document.execCommand('copy');
      textArea.remove();
    }
    log(url);
    await sendMessageToPage('success', url);
  };
})(window.chrome || window.browser);
<|endoftext|>


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

var list = require('../cmd/list.js'),
find = require('../cmd/find.js'),
howto = require('../cmd/howto.js'),
create = require('../cmd/create.js'),
remove = require('../cmd/remove.js'),
activate = require('../cmd/activate.js'),
deactivate = require('../cmd/deactivate.js');

var parsePkgConfig = require('../lib/config.js').parsePkgConfig,
parseUserConfig = require('../lib/config.js').parseUserConfig,
parseGlobalConfig = require('../lib/config.js').parseGlobalConfig,
parseEcosystemConfig = require('../lib/config.js').parseEcosystemConfig;

var envReady = require('../lib/inception.js').envReady,
rootReady = require('../lib/inception.js').rootReady,
recordReady = require('../lib/inception.js').recordReady,
activateReady = require('../lib/inception.js').activateReady;

var idUnique = require('../lib/validation.js').idUnique,
idExsit = require('../lib/validation.js').idExsit,
idValid = require('../lib/validation.js').idValid,
cmdValid = require('../lib/validation.js').cmdValid,
ecosystemActive = require('../lib/validation.js').ecosystemActive,
releaseExist = require('../lib/assistant.js').getRelease,
ecosystemExist = require('../lib/assistant.js').getEcosystem;

var log = require('../lib/display.js').log;
var message, warning, error, suggestion, example;

var argv = process.argv, id, target, cmd = argv[2];

if (cmd === undefined) {
  error = 'Missing command';
  suggestion = 'Available commands: howto, create, remove, list, find, activate, deactivate';
  example = 'neco howto, neco create <id>, neco list';
  log('error', error, suggestion, example);

} else if (!cmdValid(cmd)) {
  error = 'Not a valid command';
  suggestion = 'Available commands: howto, create, remove, list, find, activate, deactivate';
  example = 'neco hwoto, neco create <id>, neco list';
  log('error', error, suggestion, example);
} else {
  parseGlobalConfig(function() { parsePkgConfig(function() {
    parseUserConfig(function() {envReady(function() {
      rootReady(function() { activateReady(function() {
        // Subcommand create
        if (cmd === 'create') {
          if (argv.length < 4) {
            message = 'Missing ID';
            suggestion = 'Please specific at least one ID( and the version of node, if you will).';
            example = 'neco create <id> [stable, latest, node-version]';
            log('message', message, suggestion, example);
          } else {
            id = argv[3], target = argv[4] || 'stable'; // defaut target is stable
            recordReady(function(exists) {
              if (!exists) {
                create.run(id, target);
              } else {
                if (!idValid(id)) {
                  message = 'The given id '+id+' is one of the reserved words in neco.';
                  suggestion = 'Please choose another one.';
                  log('message', message, suggestion);
                } else if (!idUnique(id)) {
                  message = 'The given id '+id+' has already been used.';
                  suggestion = 'Please choose another one instead.';
                  log('message', message, suggestion);
                } else {
                  create.run(id ,target);
                }
              }
            });
          }
        }

        // Subcommand list
        else if (cmd === 'list') {
          recordReady(function(exists) {
            if (argv.length >= 4) {
              id = argv[3];
              if (ecosystemExist(id)) {
                list.run(id);
              } else {
                error = 'The desired ecosystem '+id+' is not exists.';
                suggestion = 'Find out all the existing ecosystem.';
                example = 'neco list';
                log('error', error, suggestion, example);
              }
            } else {
              list.run()
            }
          });
        }

        // Subcommand find
        else if (cmd === 'find') {
          recordReady(function(exists) {
            if (argv.length >= 4) {
              target = argv[3];
              if (releaseExist(target)) {
                find.run(target);
              } else {
                error = 'The desired release '+target+' is not available.';
                suggestion = 'Find out all the available releases.';
                example = 'neco find [stable, latest, node-version]';
                log('error', error, suggestion, example);
              }
            } else {
              find.run();
            }
          });
        }

        // Subcommand help
        else if (cmd === 'howto') {
          howto.run(config);
        }

        // Subcommand activate
        else if (cmd === 'activate') {
          if (argv.length < 4) {
            message = 'Missing ID';
            suggestion = 'Please specify the id of the ecosystem you want to activate.';
            example = 'neco activate <id>';
            log('message', message, suggestion, example);
          } else {
            id = process.argv[3];
            parseEcosystemConfig(id, function() {
              recordReady(function(exists) {
                if (ecosystemActive(config)) {
                  warning = 'The node ecosystem with id '+id+' is already active.';
                  suggstion = 'Please use type deact in your shell to deactivate it.';
                  log('warning', warning, suggestion, example);
                } else if (!idExsit(id)) {
                  warning = 'The node ecosystem with id '+id+' is not exists.';
                  suggestion = 'You can use neco list command to find out all existing ecosystem.';
                  example = 'neco create <id> [node-version]';
                  log('warning', warning, suggestion, example);
                } else {
                  activate.run(id);
                }
              });
            });
          }
        }

        // Subcommand deactvate
        else if (cmd === 'deactivate') {
          if (argv.length < 4) {
            message = 'Missing ID';
            suggestion = 'Please specify the id of the ecosystem you want to deactivate.';
            example = 'neco deactivate <id>';
            log('message', message, suggestion, example);
          } else {
            id = process.argv[3];
            parseEcosystemConfig(id, function() {
              recordReady(function(exists) {
                if (!idExsit(id)) {
                  error = 'The node ecosystem with id '+id+' is not exists.';
                  suggestion = 'You can use neco list command to find out all existing ecosystem.';
                  example = 'neco list';
                  log('error', error, suggestion, example);
                } else if (!ecosystemActive(id)) {
                  error = 'The node ecosystem with id '+id+' is not active.';
                  suggestion = 'Use neco activate command to activate it first.';
                  example = 'neco_activate '+ id;
                  log('error', error, suggestion, example);
                } else {
                  deactivate.run(id);
                }
              });
            });
          }
        }

        // Subcommand remove
        else  if (cmd === 'remove') {
          if (argv.length < 4) {
            message = 'Missing ID';
            suggestion = 'Please specific the ID of the ecosystem that you want to remove.';
            example = 'neco remove <id>';
            log('message', message, suggestion, example);
          } else {
            id = argv[3];
            parseEcosystemConfig(id, function() {
              recordReady(function(exists) {
                if (!idExsit(id)) {
                  message = 'The given id '+id+' is not exist.';
                  suggestion = 'Find out all existing ecosystem.';
                  example = 'neco list'
                  log('message', message, suggestion, example);
                } else if (ecosystemActive(id)) {
                  message = 'The given ecosystem with id '+id+' is in active.';
                  suggestion = 'Please deactivate it first.';
                  example = 'neco_deactivate'
                  log('message', message, suggestion, example);
                } else {
                  remove.run(id);
                }
              });
            });
          }
        }

      });});
    });});
  });});
}
<commit_msg>fixed miss cmd in envReady<commit_after>#!/usr/bin/env node

var list = require('../cmd/list.js'),
find = require('../cmd/find.js'),
howto = require('../cmd/howto.js'),
create = require('../cmd/create.js'),
remove = require('../cmd/remove.js'),
activate = require('../cmd/activate.js'),
deactivate = require('../cmd/deactivate.js');

var parsePkgConfig = require('../lib/config.js').parsePkgConfig,
parseUserConfig = require('../lib/config.js').parseUserConfig,
parseGlobalConfig = require('../lib/config.js').parseGlobalConfig,
parseEcosystemConfig = require('../lib/config.js').parseEcosystemConfig;

var envReady = require('../lib/inception.js').envReady,
rootReady = require('../lib/inception.js').rootReady,
recordReady = require('../lib/inception.js').recordReady,
activateReady = require('../lib/inception.js').activateReady;

var idUnique = require('../lib/validation.js').idUnique,
idExsit = require('../lib/validation.js').idExsit,
idValid = require('../lib/validation.js').idValid,
cmdValid = require('../lib/validation.js').cmdValid,
ecosystemActive = require('../lib/validation.js').ecosystemActive,
releaseExist = require('../lib/assistant.js').getRelease,
ecosystemExist = require('../lib/assistant.js').getEcosystem;

var log = require('../lib/display.js').log;
var message, warning, error, suggestion, example;

var argv = process.argv, id, target, cmd = argv[2];

if (cmd === undefined) {
  error = 'Missing command';
  suggestion = 'Available commands: howto, create, remove, list, find, activate, deactivate';
  example = 'neco howto, neco create <id>, neco list';
  log('error', error, suggestion, example);

} else if (!cmdValid(cmd)) {
  error = 'Not a valid command';
  suggestion = 'Available commands: howto, create, remove, list, find, activate, deactivate';
  example = 'neco hwoto, neco create <id>, neco list';
  log('error', error, suggestion, example);
} else {
  parseGlobalConfig(function() { parsePkgConfig(function() {
    parseUserConfig(function() {envReady(cmd, function() {
      rootReady(function() { activateReady(function() {
        // Subcommand create
        if (cmd === 'create') {
          if (argv.length < 4) {
            message = 'Missing ID';
            suggestion = 'Please specific at least one ID( and the version of node, if you will).';
            example = 'neco create <id> [stable, latest, node-version]';
            log('message', message, suggestion, example);
          } else {
            id = argv[3], target = argv[4] || 'stable'; // defaut target is stable
            recordReady(function(exists) {
              if (!exists) {
                create.run(id, target);
              } else {
                if (!idValid(id)) {
                  message = 'The given id '+id+' is one of the reserved words in neco.';
                  suggestion = 'Please choose another one.';
                  log('message', message, suggestion);
                } else if (!idUnique(id)) {
                  message = 'The given id '+id+' has already been used.';
                  suggestion = 'Please choose another one instead.';
                  log('message', message, suggestion);
                } else {
                  create.run(id ,target);
                }
              }
            });
          }
        }

        // Subcommand list
        else if (cmd === 'list') {
          recordReady(function(exists) {
            if (argv.length >= 4) {
              id = argv[3];
              if (ecosystemExist(id)) {
                list.run(id);
              } else {
                error = 'The desired ecosystem '+id+' is not exists.';
                suggestion = 'Find out all the existing ecosystem.';
                example = 'neco list';
                log('error', error, suggestion, example);
              }
            } else {
              list.run()
            }
          });
        }

        // Subcommand find
        else if (cmd === 'find') {
          recordReady(function(exists) {
            if (argv.length >= 4) {
              target = argv[3];
              if (releaseExist(target)) {
                find.run(target);
              } else {
                error = 'The desired release '+target+' is not available.';
                suggestion = 'Find out all the available releases.';
                example = 'neco find [stable, latest, node-version]';
                log('error', error, suggestion, example);
              }
            } else {
              find.run();
            }
          });
        }

        // Subcommand help
        else if (cmd === 'howto') {
          howto.run(config);
        }

        // Subcommand activate
        else if (cmd === 'activate') {
          if (argv.length < 4) {
            message = 'Missing ID';
            suggestion = 'Please specify the id of the ecosystem you want to activate.';
            example = 'neco activate <id>';
            log('message', message, suggestion, example);
          } else {
            id = process.argv[3];
            parseEcosystemConfig(id, function() {
              recordReady(function(exists) {
                if (ecosystemActive(config)) {
                  warning = 'The node ecosystem with id '+id+' is already active.';
                  suggstion = 'Please use type deact in your shell to deactivate it.';
                  log('warning', warning, suggestion, example);
                } else if (!idExsit(id)) {
                  warning = 'The node ecosystem with id '+id+' is not exists.';
                  suggestion = 'You can use neco list command to find out all existing ecosystem.';
                  example = 'neco create <id> [node-version]';
                  log('warning', warning, suggestion, example);
                } else {
                  activate.run(id);
                }
              });
            });
          }
        }

        // Subcommand deactvate
        else if (cmd === 'deactivate') {
          if (argv.length < 4) {
            message = 'Missing ID';
            suggestion = 'Please specify the id of the ecosystem you want to deactivate.';
            example = 'neco deactivate <id>';
            log('message', message, suggestion, example);
          } else {
            id = process.argv[3];
            parseEcosystemConfig(id, function() {
              recordReady(function(exists) {
                if (!idExsit(id)) {
                  error = 'The node ecosystem with id '+id+' is not exists.';
                  suggestion = 'You can use neco list command to find out all existing ecosystem.';
                  example = 'neco list';
                  log('error', error, suggestion, example);
                } else if (!ecosystemActive(id)) {
                  error = 'The node ecosystem with id '+id+' is not active.';
                  suggestion = 'Use neco activate command to activate it first.';
                  example = 'neco_activate '+ id;
                  log('error', error, suggestion, example);
                } else {
                  deactivate.run(id);
                }
              });
            });
          }
        }

        // Subcommand remove
        else  if (cmd === 'remove') {
          if (argv.length < 4) {
            message = 'Missing ID';
            suggestion = 'Please specific the ID of the ecosystem that you want to remove.';
            example = 'neco remove <id>';
            log('message', message, suggestion, example);
          } else {
            id = argv[3];
            parseEcosystemConfig(id, function() {
              recordReady(function(exists) {
                if (!idExsit(id)) {
                  message = 'The given id '+id+' is not exist.';
                  suggestion = 'Find out all existing ecosystem.';
                  example = 'neco list'
                  log('message', message, suggestion, example);
                } else if (ecosystemActive(id)) {
                  message = 'The given ecosystem with id '+id+' is in active.';
                  suggestion = 'Please deactivate it first.';
                  example = 'neco_deactivate'
                  log('message', message, suggestion, example);
                } else {
                  remove.run(id);
                }
              });
            });
          }
        }

      });});
    });});
  });});
}
<|endoftext|>


<commit_before>const passport = require('passport')
const { User } = require('./db/models')

const GitHubStrategy = require('passport-github').Strategy
const githubInfo = {
  clientID: process.env.GITHUB_CLIENT_ID,
  clientSecret: process.env.GITHUB_CLIENT_SECRET,
  callbackURL: 'http://localhost:3000/auth/github/callback',
  scope: 'user:email'
}

passport.serializeUser(function (user, done) {
  done(null, user._id)
})

passport.deserializeUser(function (id, done) {
  User.findById(id, function (err, user) {
    done(err, user)
  })
})

passport.use(new GitHubStrategy(githubInfo,
  function (accessToken, refreshToken, profile, cb) {
    User
      .findOne({
        githubId: profile.id
      })
      .then(user => {
        if (user == null) {
          return User
            .create({
              name: profile.displayName || profile.username,
              emails: profile.emails,
              githubId: profile.id,
              githubName: profile.username,
              photos: profile.photos
            })
        }
        return user
      })
      .then(user => {
        cb(null, user)
      })
      .catch(err => {
        cb(err)
      })
  }
))

module.exports = passport
<commit_msg>Update oauth callback url<commit_after>const passport = require('passport')
const { User } = require('./db/models')

const GitHubStrategy = require('passport-github').Strategy
const githubInfo = {
  clientID: process.env.GITHUB_CLIENT_ID,
  clientSecret: process.env.GITHUB_CLIENT_SECRET,
  callbackURL: process.env.BASE_URL + '/auth/github/callback',
  scope: 'user:email'
}

passport.serializeUser(function (user, done) {
  done(null, user._id)
})

passport.deserializeUser(function (id, done) {
  User.findById(id, function (err, user) {
    done(err, user)
  })
})

passport.use(new GitHubStrategy(githubInfo,
  function (accessToken, refreshToken, profile, cb) {
    User
      .findOne({
        githubId: profile.id
      })
      .then(user => {
        if (user == null) {
          return User
            .create({
              name: profile.displayName || profile.username,
              emails: profile.emails,
              githubId: profile.id,
              githubName: profile.username,
              photos: profile.photos
            })
        }
        return user
      })
      .then(user => {
        cb(null, user)
      })
      .catch(err => {
        cb(err)
      })
  }
))

module.exports = passport
<|endoftext|>


<commit_before>var m = require('mithril');

var EVAL_REGEX = new RegExp(''
  + /^info depth (\d+) seldepth \d+ multipv (\d+) /.source
  + /score (cp|mate) ([-\d]+) /.source
  + /(?:(upper|lower)bound )?nodes (\d+) nps \S+ /.source
  + /(?:hashfull \d+ )?tbhits \d+ time (\S+) /.source
  + /pv (.+)/.source);

module.exports = function(worker, opts) {
  var work = null;
  var curEval = null;
  var expectedPvs = 0;
  var engineVersion;

  var stopped = m.deferred();
  stopped.resolve(true);

  if (opts.variant.key === 'fromPosition' || opts.variant.key === 'chess960')
    worker.send('setoption name UCI_Chess960 value true');
  else if (opts.variant.key === 'antichess')
    worker.send('setoption name UCI_Variant value giveaway');
  else if (opts.variant.key !== 'standard')
    worker.send('setoption name UCI_Variant value ' + opts.variant.key.toLowerCase());
  else
    worker.send('isready'); // warm up the webworker

  var processOutput = function(text) {
    if (text.indexOf('bestmove ') === 0) {
      if (!stopped) stopped = m.deferred();
      stopped.resolve(true);
      return;
    }
    if (!work) return;

    var matches = text.match(EVAL_REGEX);
    if (!matches) {
      if (!engineVersion) {
        var m = text.match(/Stockfish (.+) by /);
        engineVersion = m && m[1];
      }
      return;
    }

    var depth = parseInt(matches[1]),
        multiPv = parseInt(matches[2]),
        isMate = matches[3] === 'mate',
        eval = parseInt(matches[4]),
        evalType = matches[5],
        nodes = parseInt(matches[6]),
        elapsedMs = parseInt(matches[7]),
        moves = matches[8].split(' ', 10);

    // time is negative on safari
    if (!elapsedMs || elapsedMs < 0) elapsedMs = (new Date() - work.startedAt)

    // Track max pv index to determine when pv prints are done.
    if (expectedPvs < multiPv) expectedPvs = multiPv;

    if (depth < opts.minDepth) return;
    if (work.ply % 2 === 1) eval = -eval;

    // For now, ignore most upperbound/lowerbound messages.
    // The exception is for multiPV, sometimes non-primary PVs
    // only have an upperbound. See: ddugovic/Stockfish#228
    if (evalType && multiPv === 1) return;

    var pvData = {
      moves: moves,
      cp: isMate ? undefined : eval,
      mate: isMate ? eval : undefined,
      depth: depth,
    };

    if (multiPv === 1) {
      curEval = {
        fen: work.currentFen,
        maxDepth: work.maxDepth,
        depth: depth,
        knps: nodes / elapsedMs,
        nodes: nodes,
        cp: pvData.cp,
        mate: pvData.mate,
        pvs: [pvData],
        millis: elapsedMs
      };
    } else if (curEval) {
      curEval.pvs.push(pvData);
      curEval.depth = Math.min(curEval.depth, depth);
    }

    if (multiPv === expectedPvs && curEval) {
      work.emit(curEval);
      curEval = null;
    }
  };

  return {
    start: function(w) {
      work = w;
      work.startedAt = new Date();
      curEval = null;
      stopped = null;
      expectedPvs = 1;
      if (opts.threads) worker.send('setoption name Threads value ' + opts.threads());
      if (opts.hashSize) worker.send('setoption name Hash value ' + opts.hashSize());
      worker.send('setoption name MultiPV value ' + work.multiPv);
      worker.send(['position', 'fen', work.initialFen, 'moves'].concat(work.moves).join(' '));
      worker.send('go depth ' + work.maxDepth);
    },
    stop: function() {
      if (!stopped) {
        work = null;
        stopped = m.deferred();
        worker.send('stop');
      }
      return stopped;
    },
    received: processOutput,
    engineVersion: function() {
      return engineVersion;
    }
  };
};
<commit_msg>fix ceval SF parser<commit_after>var m = require('mithril');

var EVAL_REGEX = new RegExp(''
  + /^info depth (\d+) seldepth \d+ multipv (\d+) /.source
  + /score (cp|mate) ([-\d]+) /.source
  + /(?:(upper|lower)bound )?nodes (\d+) nps \S+ /.source
  + /(?:hashfull \d+ )?tbhits \d+ time (\S+) /.source
  + /pv (.+)/.source);

module.exports = function(worker, opts) {
  var work = null;
  var curEval = null;
  var expectedPvs = 0;
  var engineVersion;

  var stopped = m.deferred();
  stopped.resolve(true);

  if (opts.variant.key === 'fromPosition' || opts.variant.key === 'chess960')
    worker.send('setoption name UCI_Chess960 value true');
  else if (opts.variant.key === 'antichess')
    worker.send('setoption name UCI_Variant value giveaway');
  else if (opts.variant.key !== 'standard')
    worker.send('setoption name UCI_Variant value ' + opts.variant.key.toLowerCase());
  else
    worker.send('isready'); // warm up the webworker

  var processOutput = function(text) {
    if (text.indexOf('bestmove ') === 0) {
      if (!stopped) stopped = m.deferred();
      stopped.resolve(true);
      return;
    }
    if (!work) return;

    var matches = text.match(EVAL_REGEX);
    if (!matches) {
      if (!engineVersion) {
        var match = text.match(/Stockfish (.+) by /);
        engineVersion = match && match[1];
      }
      return;
    }

    var depth = parseInt(matches[1]),
        multiPv = parseInt(matches[2]),
        isMate = matches[3] === 'mate',
        eval = parseInt(matches[4]),
        evalType = matches[5],
        nodes = parseInt(matches[6]),
        elapsedMs = parseInt(matches[7]),
        moves = matches[8].split(' ', 10);

    // time is negative on safari
    if (!elapsedMs || elapsedMs < 0) elapsedMs = (new Date() - work.startedAt)

    // Track max pv index to determine when pv prints are done.
    if (expectedPvs < multiPv) expectedPvs = multiPv;

    if (depth < opts.minDepth) return;
    if (work.ply % 2 === 1) eval = -eval;

    // For now, ignore most upperbound/lowerbound messages.
    // The exception is for multiPV, sometimes non-primary PVs
    // only have an upperbound. See: ddugovic/Stockfish#228
    if (evalType && multiPv === 1) return;

    var pvData = {
      moves: moves,
      cp: isMate ? undefined : eval,
      mate: isMate ? eval : undefined,
      depth: depth,
    };

    if (multiPv === 1) {
      curEval = {
        fen: work.currentFen,
        maxDepth: work.maxDepth,
        depth: depth,
        knps: nodes / elapsedMs,
        nodes: nodes,
        cp: pvData.cp,
        mate: pvData.mate,
        pvs: [pvData],
        millis: elapsedMs
      };
    } else if (curEval) {
      curEval.pvs.push(pvData);
      curEval.depth = Math.min(curEval.depth, depth);
    }

    if (multiPv === expectedPvs && curEval) {
      work.emit(curEval);
      curEval = null;
    }
  };

  return {
    start: function(w) {
      work = w;
      work.startedAt = new Date();
      curEval = null;
      stopped = null;
      expectedPvs = 1;
      if (opts.threads) worker.send('setoption name Threads value ' + opts.threads());
      if (opts.hashSize) worker.send('setoption name Hash value ' + opts.hashSize());
      worker.send('setoption name MultiPV value ' + work.multiPv);
      worker.send(['position', 'fen', work.initialFen, 'moves'].concat(work.moves).join(' '));
      worker.send('go depth ' + work.maxDepth);
    },
    stop: function() {
      if (!stopped) {
        work = null;
        stopped = m.deferred();
        worker.send('stop');
      }
      return stopped;
    },
    received: processOutput,
    engineVersion: function() {
      return engineVersion;
    }
  };
};
<|endoftext|>


<commit_before>import { connect } from 'react-redux';
import {
  makeSelectClaimIsMine,
  makeSelectFileInfoForUri,
  makeSelectClaimForUri,
  doPrepareEdit,
  selectMyChannelClaims,
} from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { doSetPlayingUri } from 'redux/actions/content';
import { doToast } from 'redux/actions/notifications';
import { doOpenModal } from 'redux/actions/app';
import fs from 'fs';
import FileActions from './view';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';

const select = (state, props) => ({
  claim: makeSelectClaimForUri(props.uri)(state),
  claimIsMine: makeSelectClaimIsMine(props.uri)(state),
  fileInfo: makeSelectFileInfoForUri(props.uri)(state),
  renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
  costInfo: makeSelectCostInfoForUri(props.uri)(state),
  myChannels: selectMyChannelClaims(state),
});

const perform = dispatch => ({
  openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
  prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
  clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
  doToast: options => dispatch(doToast(options)),
});

export default connect(select, perform)(FileActions);
<commit_msg>correctly set active channel on edits<commit_after>import { connect } from 'react-redux';
import {
  makeSelectClaimIsMine,
  makeSelectFileInfoForUri,
  makeSelectClaimForUri,
  doPrepareEdit,
  selectMyChannelClaims,
} from 'lbry-redux';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { doSetPlayingUri } from 'redux/actions/content';
import { doToast } from 'redux/actions/notifications';
import { doOpenModal, doSetActiveChannel, doSetIncognito } from 'redux/actions/app';
import fs from 'fs';
import FileActions from './view';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';

const select = (state, props) => ({
  claim: makeSelectClaimForUri(props.uri)(state),
  claimIsMine: makeSelectClaimIsMine(props.uri)(state),
  fileInfo: makeSelectFileInfoForUri(props.uri)(state),
  renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
  costInfo: makeSelectCostInfoForUri(props.uri)(state),
  myChannels: selectMyChannelClaims(state),
});

const perform = (dispatch) => ({
  openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
  prepareEdit: (publishData, uri, fileInfo) => {
    if (publishData.signing_channel) {
      dispatch(doSetActiveChannel(publishData.signing_channel.claim_id));
    } else {
      dispatch(doSetIncognito(true));
    }

    dispatch(doPrepareEdit(publishData, uri, fileInfo, fs));
  },
  clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
  doToast: (options) => dispatch(doToast(options)),
});

export default connect(select, perform)(FileActions);
<|endoftext|>


<commit_before>/*
WYSIWYG arguments

enableKeyboardExtras {boolean} enable creating new components on enter, and appending text to previous components on delete, etc
buttons {array} array of button names (strings) for tooltip
styled {boolean} apply input styles to contenteditable element
 */

var _ = require('lodash'),
  select = require('selection-range'),
  MediumEditor = require('medium-editor'),
  MediumButton = require('../services/medium-button'),
  dom = require('../services/dom'),
  db = require('../services/db'),
  render = require('../services/render'),
  focus = require('../decorators/focus'),
  references = require('../services/references'),
  edit = require('../services/edit');

/**
 * toggle the tiered toolbar
 * this is the action for the tieredToolbar extension
 * @param {Element} html
 * @returns {Element}
 */
function toggleTieredToolbar(html) {
  // note: this element doesn't exist before it's instantiated, so we need to grab it afterwards
  var toolbar = dom.find('.medium-editor-toolbar'),
    toolbarClasses = toolbar.classList;

  if (toolbarClasses.contains('show-none')) {
    toolbarClasses.remove('show-none');
  } else if (toolbarClasses.contains('show-all')) {
    toolbarClasses.add('show-none');
  } else {
    toolbarClasses.add('show-all');
  }
  return html;
}

/**
 * create new medium editor
 * @param {Element} field
 * @param {array} buttons
 * @returns {Element}
 */
function createEditor(field, buttons) {
  return new MediumEditor(field, {
    toolbar: {
      // buttons that go in the toolbar
      buttons: buttons,
      standardizeSelectionStart: true,
      allowMultiParagraphSelection: false
    },
    delay: 200, // wait a bit for the toolbar and link previews to display
    paste: {
      forcePlainText: true,
      cleanPastedHTML: false, // clean html from sources like google docs
      cleanTags: [ // remove these tags when pasting
        'meta',
        'script',
        'style',
        'img',
        'object',
        'iframe'
      ],
      cleanReplacements: [
        [/<h[2-9]>/ig, '
'],
        [/<\/h[2-9]>/ig, '
'], // force all headers to the same level
        [/
/ig, ''], // get rid of
 tags
        [/<\/p>/ig, '']
      ]
    },
    autoLink: true, // create links automatically when urls are entered
    imageDragging: false, // disallow dragging inline images
    targetBlank: true,
    disableReturn: true,
    placeholder: false, // the placeholder isn't native
    extensions: {
      tieredToolbar: new MediumButton({
        label: '…',
        action: toggleTieredToolbar
      })
    }
  });
}

/**
 * get elements and data for the current component
 * @param {Element} el
 * @returns {object}
 */
function getCurrent(el) {
  var currentComponent = dom.closest(el, '[' + references.referenceAttribute + ']'),
    currentComponentRef = currentComponent.getAttribute(references.referenceAttribute);

  return {
    field: el.getAttribute(references.fieldAttribute),
    component: currentComponent,
    ref: currentComponentRef,
    name: references.getComponentNameFromReference(currentComponentRef)
  };
}

/**
 * get elements and data for the parent component
 * @param {Element} el of the current component
 * @returns {object}
 */
function getParent(el) {
  var parentNode = el.parentNode,
    parentComponent = dom.closest(parentNode, '[' + references.referenceAttribute + ']'),
    parentComponentRef = parentComponent.getAttribute(references.referenceAttribute);

  return {
    field: dom.closest(parentNode, '[' + references.editableAttribute + ']').getAttribute(references.editableAttribute),
    component: parentComponent,
    ref: parentComponentRef,
    name: references.getComponentNameFromReference(parentComponentRef)
  };
}

/**
 * get previous component in list, if any
 * @param {object} current
 * @param {object} parent
 * @returns {Promise} with {_ref: previous ref} or undefined
 */
function getPrev(current, parent) {
  return edit.getDataOnly(parent.ref).then(function (parentData) {
    var index = _.findIndex(parentData[parent.field], { _ref: current.ref }),
      before = _.take(parentData[parent.field], index),
      prev = _.findLast(before, function (component) {
        return references.getComponentNameFromReference(component._ref) === current.name;
      });

    if (prev) {
      return {
        field: current.field,
        component: dom.find(parent.component, '[' + references.referenceAttribute + '="' + prev._ref + '"]'),
        ref: prev._ref,
        name: current.name
      };
    }
  });
}

/**
 * get the contents of a wysiwyg field
 * note: if we want to remove / parse / sanitize contents when doing operations,
 * this is the place we should do it
 * @param {Element} el
 * @returns {string}
 */
function getFieldContents(el) {
  return el.innerHTML;
}

/**
 * append text/html to previous component's field
 * @param {string} html
 * @param {object} prev
 * @returns {Promise}
 */
function appendToPrev(html, prev) {
  // note: get fresh data from the server
  return db.getComponentJSONFromReference(prev.ref).then(function (prevData) {
    var prevFieldData = _.get(prevData, prev.field);

    // add current field's html to the end of the previous field
    prevFieldData += html;
    // then put it back into the previous component's data
    _.set(prevData, prev.field, prevFieldData);
    return db.putToReference(prev.ref, prevData);
  });
}

/**
 * remove current component from parent
 * @param {object} current
 * @param {object} parent
 * @returns {Function}
 */
function removeCurrentFromParent(current, parent) {
  return function () {
    return db.getComponentJSONFromReference(parent.ref).then(function (parentData) {
      var index = _.findIndex(parentData[parent.field], { _ref: current.ref });

      parentData[parent.field].splice(index, 1); // splice the current component out of the parent
      dom.removeElement(current.component); // remove component el
      return db.putToReference(parent.ref, parentData);
    });
  };
}

function reloadPreviousComponent(prev) {
  return function () {
    return db.getComponentHTMLFromReference(prev.ref)
      .then(function (updatedEl) {
        render.addComponentsHandlers(updatedEl);
        dom.replaceElement(prev.component, updatedEl);
        return updatedEl;
      });
  };
}

/**
 * focus on the previous component's field
 * @param {object} parent
 * @param  {object} prev
 * @returns {Function}
 */
function focusPreviousComponent(parent, prev) {
  return function (newEl) {
    return focus.focus(newEl, { ref: prev.ref, path: prev.field }).then(function () {
      dom.find('[data-ref="' + prev.ref + '"] [data-field]').focus();
    });
  };
}

/**
 * remove current component, append text to previous component (of the same name)
 * @param {Element} el
 * @returns {Promise|undefined}
 */
function removeComponent(el) {
  var current = getCurrent(el),
    parent = getParent(current.component);

  // find the previous component, if any
  return getPrev(current, parent).then(function (prev) {
    if (prev) {
      // there's a previous component with the same name!
      // get the contents of the current field, and append them to the previous component
      return appendToPrev(getFieldContents(el), prev)
        .then(removeCurrentFromParent(current, parent))
        .then(reloadPreviousComponent(prev))
        .then(focusPreviousComponent(parent, prev));
    }
  });
}

/**
 * add component after current component
 * @param {Element} el
 * @returns {Promise}
 */
function addComponent(el) {
  var current = getCurrent(el);

  // create a new component
  return db.postToReference('/components/' + current.name + '/instances', {}).then(function (res) {
    var newRef = res._ref;

    // get the html of that new component
    return db.getComponentHTMLFromReference(newRef).then(function (newEl) {
      // add the handlers for the new component
      render.addComponentsHandlers(newEl);
      // then add it after the current one
      dom.insertAfter(current.component, newEl);
      // then focus() the new field that's the same as the current field
      focus.focus(newEl, { ref: newRef, path: current.field }).then(function () {
        dom.find('[data-ref="' + newRef + '"] [data-field]').focus();
      });

      return newRef;
    });
  }).then(function (ref) { // update the parent component's component list
    var parent = getParent(current.component);

    return edit.getDataOnly(parent.ref).then(function (parentData) {
      var index = _.findIndex(parentData[parent.field], { _ref: current.ref }) + 1;

      parentData[parent.field].splice(index, 0, { _ref: ref }); // splice the new component into the array after the current one
      return db.putToReference(parent.ref, parentData);
    });
  });
}

/**
 * remove current component if we're at the beginning of the field
 * and there's a previous component to append it to
 * @param {Element} el
 * @param {KeyboardEvent} e
 * @returns {undefined|Promise}
 */
function handleComponentDeletion(el, e) {
  var caretPos = select(el);

  if (caretPos.start === 0) {
    e.preventDefault(); // stop page reload
    return removeComponent(el);
  }
}

/**
 * create new component if we're at the end of the field
 * @param {Element} el
 * @returns {false|Promise}
 */
function handleComponentCreation(el) {
  var caretPos = select(el); // get text after the cursor, if any

  // if there's stuff after the caret, get it
  if (caretPos.start < el.innerText.length - 2) {
    console.log(el.innerText.substr(caretPos.start));
    // todo: split paragraphs, add new component with text after caret
    return false; // don't do anything if you're not at the end
  } else {
    return addComponent(el);
  }
}

function isStyled(styled) {
  return styled ? ' styled' : ''; // note the preeeding space!
}

module.exports = function (result, args) {
  var rivets = result.rivets,
    buttons = args.buttons,
    styled = args.styled,
    enableKeyboardExtras = args.enableKeyboardExtras,
    textInput = dom.find(result.el, 'input') || dom.find(result.el, 'textarea'),
    field = dom.create(`<p class="wysiwyg-input${ isStyled(styled) }" data-field="${result.bindings.name}" rv-wysiwyg="data.value"></p>`);

  // if more than 5 buttons, put the rest on the second tier
  if (buttons.length > 5) {
    buttons.splice(5, 0, 'tieredToolbar'); // clicking this expands the toolbar with a second tier
  }

  // put the rich text field after the input
  dom.replaceElement(textInput, field);

  rivets.binders.wysiwyg = {
    publish: true,
    bind: function (el) {
      // this is called when the binder initializes
      var observer = this.observer,
        data = observer.value() || '', // don't print 'undefined' if there's no data
        editor = createEditor(field, buttons);

      // put the initial data into the editor
      el.innerHTML = data;

      // hide the tier2 buttons when closing the toolbar
      editor.subscribe('hideToolbar', function () {
        dom.find('.medium-editor-toolbar').classList.remove('show-all');
        dom.find('.medium-editor-toolbar').classList.remove('show-none');
      });

      // persist editor data to data model on input
      editor.subscribe('editableInput', function (e, editable) {
        observer.setValue(editable.innerHTML);
      });

      // persist editor data to data model on paste
      editor.subscribe('editablePaste', function (e, editable) {
        observer.setValue(editable.innerHTML);
      });

      editor.subscribe('editableKeydownDelete', function (e, editable) {
        if (enableKeyboardExtras) {
          handleComponentDeletion(editable, e);
        }
      });

      editor.subscribe('editableKeydownEnter', function (e, editable) {
        e.preventDefault(); // stop it from creating new paragraphs

        if (enableKeyboardExtras) {
          handleComponentCreation(editable);
        } else {
          // close the form?
          focus.unfocus();
        }
      });
    }
  };

  return result;
};
<commit_msg>added jsdoc<commit_after>/*
WYSIWYG arguments

enableKeyboardExtras {boolean} enable creating new components on enter, and appending text to previous components on delete, etc
buttons {array} array of button names (strings) for tooltip
styled {boolean} apply input styles to contenteditable element
 */

var _ = require('lodash'),
  select = require('selection-range'),
  MediumEditor = require('medium-editor'),
  MediumButton = require('../services/medium-button'),
  dom = require('../services/dom'),
  db = require('../services/db'),
  render = require('../services/render'),
  focus = require('../decorators/focus'),
  references = require('../services/references'),
  edit = require('../services/edit');

/**
 * toggle the tiered toolbar
 * this is the action for the tieredToolbar extension
 * @param {Element} html
 * @returns {Element}
 */
function toggleTieredToolbar(html) {
  // note: this element doesn't exist before it's instantiated, so we need to grab it afterwards
  var toolbar = dom.find('.medium-editor-toolbar'),
    toolbarClasses = toolbar.classList;

  if (toolbarClasses.contains('show-none')) {
    toolbarClasses.remove('show-none');
  } else if (toolbarClasses.contains('show-all')) {
    toolbarClasses.add('show-none');
  } else {
    toolbarClasses.add('show-all');
  }
  return html;
}

/**
 * create new medium editor
 * @param {Element} field
 * @param {array} buttons
 * @returns {Element}
 */
function createEditor(field, buttons) {
  return new MediumEditor(field, {
    toolbar: {
      // buttons that go in the toolbar
      buttons: buttons,
      standardizeSelectionStart: true,
      allowMultiParagraphSelection: false
    },
    delay: 200, // wait a bit for the toolbar and link previews to display
    paste: {
      forcePlainText: true,
      cleanPastedHTML: false, // clean html from sources like google docs
      cleanTags: [ // remove these tags when pasting
        'meta',
        'script',
        'style',
        'img',
        'object',
        'iframe'
      ],
      cleanReplacements: [
        [/<h[2-9]>/ig, '
'],
        [/<\/h[2-9]>/ig, '
'], // force all headers to the same level
        [/
/ig, ''], // get rid of
 tags
        [/<\/p>/ig, '']
      ]
    },
    autoLink: true, // create links automatically when urls are entered
    imageDragging: false, // disallow dragging inline images
    targetBlank: true,
    disableReturn: true,
    placeholder: false, // the placeholder isn't native
    extensions: {
      tieredToolbar: new MediumButton({
        label: '…',
        action: toggleTieredToolbar
      })
    }
  });
}

/**
 * get elements and data for the current component
 * @param {Element} el
 * @returns {object}
 */
function getCurrent(el) {
  var currentComponent = dom.closest(el, '[' + references.referenceAttribute + ']'),
    currentComponentRef = currentComponent.getAttribute(references.referenceAttribute);

  return {
    field: el.getAttribute(references.fieldAttribute),
    component: currentComponent,
    ref: currentComponentRef,
    name: references.getComponentNameFromReference(currentComponentRef)
  };
}

/**
 * get elements and data for the parent component
 * @param {Element} el of the current component
 * @returns {object}
 */
function getParent(el) {
  var parentNode = el.parentNode,
    parentComponent = dom.closest(parentNode, '[' + references.referenceAttribute + ']'),
    parentComponentRef = parentComponent.getAttribute(references.referenceAttribute);

  return {
    field: dom.closest(parentNode, '[' + references.editableAttribute + ']').getAttribute(references.editableAttribute),
    component: parentComponent,
    ref: parentComponentRef,
    name: references.getComponentNameFromReference(parentComponentRef)
  };
}

/**
 * get previous component in list, if any
 * @param {object} current
 * @param {object} parent
 * @returns {Promise} with {_ref: previous ref} or undefined
 */
function getPrev(current, parent) {
  return edit.getDataOnly(parent.ref).then(function (parentData) {
    var index = _.findIndex(parentData[parent.field], { _ref: current.ref }),
      before = _.take(parentData[parent.field], index),
      prev = _.findLast(before, function (component) {
        return references.getComponentNameFromReference(component._ref) === current.name;
      });

    if (prev) {
      return {
        field: current.field,
        component: dom.find(parent.component, '[' + references.referenceAttribute + '="' + prev._ref + '"]'),
        ref: prev._ref,
        name: current.name
      };
    }
  });
}

/**
 * get the contents of a wysiwyg field
 * note: if we want to remove / parse / sanitize contents when doing operations,
 * this is the place we should do it
 * @param {Element} el
 * @returns {string}
 */
function getFieldContents(el) {
  return el.innerHTML;
}

/**
 * append text/html to previous component's field
 * @param {string} html
 * @param {object} prev
 * @returns {Promise}
 */
function appendToPrev(html, prev) {
  // note: get fresh data from the server
  return db.getComponentJSONFromReference(prev.ref).then(function (prevData) {
    var prevFieldData = _.get(prevData, prev.field);

    // add current field's html to the end of the previous field
    prevFieldData += html;
    // then put it back into the previous component's data
    _.set(prevData, prev.field, prevFieldData);
    return db.putToReference(prev.ref, prevData);
  });
}

/**
 * remove current component from parent
 * @param {object} current
 * @param {object} parent
 * @returns {Function}
 */
function removeCurrentFromParent(current, parent) {
  return function () {
    return db.getComponentJSONFromReference(parent.ref).then(function (parentData) {
      var index = _.findIndex(parentData[parent.field], { _ref: current.ref });

      parentData[parent.field].splice(index, 1); // splice the current component out of the parent
      dom.removeElement(current.component); // remove component el
      return db.putToReference(parent.ref, parentData);
    });
  };
}

/**
 * reload previous component
 * note: this differs from the reload / render function in that it only reloads
 * a single instance of that component on the page
 * @param {object} prev
 * @returns {Function}
 */
function reloadPreviousComponent(prev) {
  return function () {
    return db.getComponentHTMLFromReference(prev.ref)
      .then(function (updatedEl) {
        render.addComponentsHandlers(updatedEl);
        dom.replaceElement(prev.component, updatedEl);
        return updatedEl;
      });
  };
}

/**
 * focus on the previous component's field
 * @param {object} parent
 * @param  {object} prev
 * @returns {Function}
 */
function focusPreviousComponent(parent, prev) {
  return function (newEl) {
    return focus.focus(newEl, { ref: prev.ref, path: prev.field }).then(function () {
      dom.find('[data-ref="' + prev.ref + '"] [data-field]').focus();
    });
  };
}

/**
 * remove current component, append text to previous component (of the same name)
 * @param {Element} el
 * @returns {Promise|undefined}
 */
function removeComponent(el) {
  var current = getCurrent(el),
    parent = getParent(current.component);

  // find the previous component, if any
  return getPrev(current, parent).then(function (prev) {
    if (prev) {
      // there's a previous component with the same name!
      // get the contents of the current field, and append them to the previous component
      return appendToPrev(getFieldContents(el), prev)
        .then(removeCurrentFromParent(current, parent))
        .then(reloadPreviousComponent(prev))
        .then(focusPreviousComponent(parent, prev));
    }
  });
}

/**
 * add component after current component
 * @param {Element} el
 * @returns {Promise}
 */
function addComponent(el) {
  var current = getCurrent(el);

  // create a new component
  return db.postToReference('/components/' + current.name + '/instances', {}).then(function (res) {
    var newRef = res._ref;

    // get the html of that new component
    return db.getComponentHTMLFromReference(newRef).then(function (newEl) {
      // add the handlers for the new component
      render.addComponentsHandlers(newEl);
      // then add it after the current one
      dom.insertAfter(current.component, newEl);
      // then focus() the new field that's the same as the current field
      focus.focus(newEl, { ref: newRef, path: current.field }).then(function () {
        dom.find('[data-ref="' + newRef + '"] [data-field]').focus();
      });

      return newRef;
    });
  }).then(function (ref) { // update the parent component's component list
    var parent = getParent(current.component);

    return edit.getDataOnly(parent.ref).then(function (parentData) {
      var index = _.findIndex(parentData[parent.field], { _ref: current.ref }) + 1;

      parentData[parent.field].splice(index, 0, { _ref: ref }); // splice the new component into the array after the current one
      return db.putToReference(parent.ref, parentData);
    });
  });
}

/**
 * remove current component if we're at the beginning of the field
 * and there's a previous component to append it to
 * @param {Element} el
 * @param {KeyboardEvent} e
 * @returns {undefined|Promise}
 */
function handleComponentDeletion(el, e) {
  var caretPos = select(el);

  if (caretPos.start === 0) {
    e.preventDefault(); // stop page reload
    return removeComponent(el);
  }
}

/**
 * create new component if we're at the end of the field
 * @param {Element} el
 * @returns {false|Promise}
 */
function handleComponentCreation(el) {
  var caretPos = select(el); // get text after the cursor, if any

  // if there's stuff after the caret, get it
  if (caretPos.start < el.innerText.length - 2) {
    console.log(el.innerText.substr(caretPos.start));
    // todo: split paragraphs, add new component with text after caret
    return false; // don't do anything if you're not at the end
  } else {
    return addComponent(el);
  }
}

function isStyled(styled) {
  return styled ? ' styled' : ''; // note the preeeding space!
}

module.exports = function (result, args) {
  var rivets = result.rivets,
    buttons = args.buttons,
    styled = args.styled,
    enableKeyboardExtras = args.enableKeyboardExtras,
    textInput = dom.find(result.el, 'input') || dom.find(result.el, 'textarea'),
    field = dom.create(`<p class="wysiwyg-input${ isStyled(styled) }" data-field="${result.bindings.name}" rv-wysiwyg="data.value"></p>`);

  // if more than 5 buttons, put the rest on the second tier
  if (buttons.length > 5) {
    buttons.splice(5, 0, 'tieredToolbar'); // clicking this expands the toolbar with a second tier
  }

  // put the rich text field after the input
  dom.replaceElement(textInput, field);

  rivets.binders.wysiwyg = {
    publish: true,
    bind: function (el) {
      // this is called when the binder initializes
      var observer = this.observer,
        data = observer.value() || '', // don't print 'undefined' if there's no data
        editor = createEditor(field, buttons);

      // put the initial data into the editor
      el.innerHTML = data;

      // hide the tier2 buttons when closing the toolbar
      editor.subscribe('hideToolbar', function () {
        dom.find('.medium-editor-toolbar').classList.remove('show-all');
        dom.find('.medium-editor-toolbar').classList.remove('show-none');
      });

      // persist editor data to data model on input
      editor.subscribe('editableInput', function (e, editable) {
        observer.setValue(editable.innerHTML);
      });

      // persist editor data to data model on paste
      editor.subscribe('editablePaste', function (e, editable) {
        observer.setValue(editable.innerHTML);
      });

      editor.subscribe('editableKeydownDelete', function (e, editable) {
        if (enableKeyboardExtras) {
          handleComponentDeletion(editable, e);
        }
      });

      editor.subscribe('editableKeydownEnter', function (e, editable) {
        e.preventDefault(); // stop it from creating new paragraphs

        if (enableKeyboardExtras) {
          handleComponentCreation(editable);
        } else {
          // close the form?
          focus.unfocus();
        }
      });
    }
  };

  return result;
};
<|endoftext|>