15

コンテンツ全体に散在する外部リンクを含む Google Docs/Drive (段落、リスト、表など) の「通常のドキュメント」を考えると、Google Apps Script を使用して存在するリンクのリストをどのようにコンパイルしますか?

具体的には、各 URL でoldTextを検索してドキュメント内の壊れたリンクをすべて更新し、各 URL でテキストではなく newText に置き換えたいと考えています。

Dev Documentation の置換テキストセクションが必要だとは思いません。ドキュメントのすべての要素をスキャンする必要がありますか? editAsTextだけで html 正規表現を使用できますか? 例をいただければ幸いです。

4

7 に答える 7

19

これはほとんど痛いだけです!コードはgist の一部として利用できます。

スクリーンショットええ、私は綴ることができません。

getAllLinks

ドキュメントをスキャンしてすべての LinkUrl を取得し、それらを配列で返すユーティリティ関数を次に示します。

/**
 * Get an array of all LinkUrls in the document. The function is
 * recursive, and if no element is provided, it will default to
 * the active document's Body element.
 *
 * @param {Element} element The document element to operate on. 
 * .
 * @returns {Array}         Array of objects, vis
 *                              {element,
 *                               startOffset,
 *                               endOffsetInclusive, 
 *                               url}
 */
function getAllLinks(element) {
  var links = [];
  element = element || DocumentApp.getActiveDocument().getBody();
  
  if (element.getType() === DocumentApp.ElementType.TEXT) {
    var textObj = element.editAsText();
    var text = element.getText();
    var inUrl = false;
    for (var ch=0; ch < text.length; ch++) {
      var url = textObj.getLinkUrl(ch);
      if (url != null) {
        if (!inUrl) {
          // We are now!
          inUrl = true;
          var curUrl = {};
          curUrl.element = element;
          curUrl.url = String( url ); // grab a copy
          curUrl.startOffset = ch;
        }
        else {
          curUrl.endOffsetInclusive = ch;
        }          
      }
      else {
        if (inUrl) {
          // Not any more, we're not.
          inUrl = false;
          links.push(curUrl);  // add to links
          curUrl = {};
        }
      }
    }
    if (inUrl) {
      // in case the link ends on the same char that the element does
      links.push(curUrl); 
    }
  }
  else {
    var numChildren = element.getNumChildren();
    for (var i=0; i<numChildren; i++) {
      links = links.concat(getAllLinks(element.getChild(i)));
    }
  }

  return links;
}

findAndReplaceLinks

getAllLinksこのユーティリティは、検索と置換機能を実行するために構築されています。

/**
 * Replace all or part of UrlLinks in the document.
 *
 * @param {String} searchPattern    the regex pattern to search for 
 * @param {String} replacement      the text to use as replacement
 *
 * @returns {Number}                number of Urls changed 
 */
function findAndReplaceLinks(searchPattern,replacement) {
  var links = getAllLinks();
  var numChanged = 0;
  
  for (var l=0; l<links.length; l++) {
    var link = links[l];
    if (link.url.match(searchPattern)) {
      // This link needs to be changed
      var newUrl = link.url.replace(searchPattern,replacement);
      link.element.setLinkUrl(link.startOffset, link.endOffsetInclusive, newUrl);
      numChanged++
    }
  }
  return numChanged;
}

デモ UI

これらのユーティリティの使用方法を示すために、いくつかの UI 拡張機能を次に示します。

function onOpen() {
  // Add a menu with some items, some separators, and a sub-menu.
  DocumentApp.getUi().createMenu('Utils')
      .addItem('List Links', 'sidebarLinks')
      .addItem('Replace Link Text', 'searchReplaceLinks')
      .addToUi();
}

function searchReplaceLinks() {
  var ui = DocumentApp.getUi();
  var app = UiApp.createApplication()
                 .setWidth(250)
                 .setHeight(100)
                 .setTitle('Change Url text');
  var form = app.createFormPanel();
  var flow = app.createFlowPanel();
  flow.add(app.createLabel("Find: "));
  flow.add(app.createTextBox().setName("searchPattern"));
  flow.add(app.createLabel("Replace: "));
  flow.add(app.createTextBox().setName("replacement"));
  var handler = app.createServerHandler('myClickHandler');
  flow.add(app.createSubmitButton("Submit").addClickHandler(handler));
  form.add(flow);
  app.add(form);
  ui.showDialog(app);
}

// ClickHandler to close dialog
function myClickHandler(e) {
  var app = UiApp.getActiveApplication();

  app.close();
  return app;
}

function doPost(e) {
  var numChanged = findAndReplaceLinks(e.parameter.searchPattern,e.parameter.replacement);
  var ui = DocumentApp.getUi();
  var app = UiApp.createApplication();
  
  sidebarLinks(); // Update list

  var result = DocumentApp.getUi().alert(
      'Results',
      "Changed "+numChanged+" urls.",
      DocumentApp.getUi().ButtonSet.OK);
}


/**
 * Shows a custom HTML user interface in a sidebar in the Google Docs editor.
 */
function sidebarLinks() {
  var links = getAllLinks();
  var sidebar = HtmlService
          .createHtmlOutput()
          .setTitle('URL Links')
          .setWidth(350 /* pixels */);

  // Display list of links, url only.
  for (var l=0; l<links.length; l++) {
    var link = links[l];
    sidebar.append('<p>'+link.url);
  }
  
  DocumentApp.getUi().showSidebar(sidebar);
}
于 2013-09-11T02:15:17.250 に答える
8

ドキュメントの本文内のすべてのリンクを反復処理することに関する、最初の質問に対する別の短い回答を提供します。textこの有益なコードは、現在のドキュメントの本文内のリンクのフラットな配列を返します。各リンクは、テキスト要素 ( )、それが含まれる段落要素またはリスト項目要素 ( paragraph)、オフセット インデックスを指すエントリを持つオブジェクトによって表されます。リンクが表示されるテキスト ( startOffset) と URL 自体 ( url)。うまくいけば、自分のニーズに簡単に合わせることができるでしょう。

テキストのすべての文字を反復するのではなく、この方法を使用するためgetTextAttributeIndices()、以前に作成された回答よりもはるかに高速に実行されることが期待されます。

編集:最初にこの回答を投稿して以来、関数を数回変更しました。また、(1)各リンクのプロパティが含まれています(テキスト要素の最後まで伸びるリンクのendOffsetInclusive場合があることに注意してください-この場合、代わりに使用できます)。(2) 本文だけでなく、ドキュメントのすべてのセクションでリンクを検索し、(3)リンクの場所を示す プロパティとプロパティを含めます。(4) 引数 を受け入れます。これを true に設定すると、同じ URL にリンクされたテキストの連続ストレッチに対して 1 つのリンク エントリのみが返されます (たとえば、テキストの一部のスタイルが別の部分)。nulllink.text.length-1sectionisFirstPageSectionmergeAdjacent

すべてのセクションにリンクを含める目的で、新しいユーティリティ関数iterateSections()が導入されました。

/**
 * Returns a flat array of links which appear in the active document's body. 
 * Each link is represented by a simple Javascript object with the following 
 * keys:
 *   - "section": {ContainerElement} the document section in which the link is
 *     found. 
 *   - "isFirstPageSection": {Boolean} whether the given section is a first-page
 *     header/footer section.
 *   - "paragraph": {ContainerElement} contains a reference to the Paragraph 
 *     or ListItem element in which the link is found.
 *   - "text": the Text element in which the link is found.
 *   - "startOffset": {Number} the position (offset) in the link text begins.
 *   - "endOffsetInclusive": the position of the last character of the link
 *      text, or null if the link extends to the end of the text element.
 *   - "url": the URL of the link.
 *
 * @param {boolean} mergeAdjacent Whether consecutive links which carry 
 *     different attributes (for any reason) should be returned as a single 
 *     entry.
 * 
 * @returns {Array} the aforementioned flat array of links.
 */
function getAllLinks(mergeAdjacent) {
  var links = [];

  var doc = DocumentApp.getActiveDocument();


  iterateSections(doc, function(section, sectionIndex, isFirstPageSection) {
    if (!("getParagraphs" in section)) {
      // as we're using some undocumented API, adding this to avoid cryptic
      // messages upon possible API changes.
      throw new Error("An API change has caused this script to stop " + 
                      "working.\n" +
                      "Section #" + sectionIndex + " of type " + 
                      section.getType() + " has no .getParagraphs() method. " +
        "Stopping script.");
    }

    section.getParagraphs().forEach(function(par) { 
      // skip empty paragraphs
      if (par.getNumChildren() == 0) {
        return;
      }

      // go over all text elements in paragraph / list-item
      for (var el=par.getChild(0); el!=null; el=el.getNextSibling()) {
        if (el.getType() != DocumentApp.ElementType.TEXT) {
          continue;
        }

        // go over all styling segments in text element
        var attributeIndices = el.getTextAttributeIndices();
        var lastLink = null;
        attributeIndices.forEach(function(startOffset, i, attributeIndices) { 
          var url = el.getLinkUrl(startOffset);

          if (url != null) {
            // we hit a link
            var endOffsetInclusive = (i+1 < attributeIndices.length? 
                                      attributeIndices[i+1]-1 : null);

            // check if this and the last found link are continuous
            if (mergeAdjacent && lastLink != null && lastLink.url == url && 
                  lastLink.endOffsetInclusive == startOffset - 1) {
              // this and the previous style segment are continuous
              lastLink.endOffsetInclusive = endOffsetInclusive;
              return;
            }

            lastLink = {
              "section": section,
              "isFirstPageSection": isFirstPageSection,
              "paragraph": par,
              "textEl": el,
              "startOffset": startOffset,
              "endOffsetInclusive": endOffsetInclusive,
              "url": url
            };

            links.push(lastLink);
          }        
        });
      }
    });
  });


  return links;
}

/**
 * Calls the given function for each section of the document (body, header, 
 * etc.). Sections are children of the DocumentElement object.
 *
 * @param {Document} doc The Document object (such as the one obtained via
 *     a call to DocumentApp.getActiveDocument()) with the sections to iterate
 *     over.
 * @param {Function} func A callback function which will be called, for each
 *     section, with the following arguments (in order):
 *       - {ContainerElement} section - the section element
 *       - {Number} sectionIndex - the child index of the section, such that
 *         doc.getBody().getParent().getChild(sectionIndex) == section.
 *       - {Boolean} isFirstPageSection - whether the section is a first-page
 *         header/footer section.
 */
function iterateSections(doc, func) {
  // get the DocumentElement interface to iterate over all sections
  // this bit is undocumented API
  var docEl = doc.getBody().getParent();

  var regularHeaderSectionIndex = (doc.getHeader() == null? -1 : 
                                   docEl.getChildIndex(doc.getHeader()));
  var regularFooterSectionIndex = (doc.getFooter() == null? -1 : 
                                   docEl.getChildIndex(doc.getFooter()));

  for (var i=0; i<docEl.getNumChildren(); ++i) {
    var section = docEl.getChild(i);

    var sectionType = section.getType();
    var uniqueSectionName;
    var isFirstPageSection = (
      i != regularHeaderSectionIndex &&
      i != regularFooterSectionIndex && 
      (sectionType == DocumentApp.ElementType.HEADER_SECTION ||
       sectionType == DocumentApp.ElementType.FOOTER_SECTION));

    func(section, i, isFirstPageSection);
  }
}
于 2016-11-21T21:52:41.703 に答える
1

私は遊んでいて、@ Mogsdadの答えを取り入れました-これは本当に複雑なバージョンです:

var _ = Underscorejs.load(); // loaded via http://googleappsdeveloper.blogspot.com/2012/11/using-open-source-libraries-in-apps.html, rolled my own
var ui = DocumentApp.getUi();

// #region --------------------- Utilities -----------------------------

var gDocsHelper = (function(P, un) {
  // heavily based on answer https://stackoverflow.com/a/18731628/1037948

  var updatedLinkText = function(link, offset) {
    return function() { return 'Text: ' + link.getText().substring(offset,100) + ((link.getText().length-offset) > 100 ? '...' : ''); }
  }

  P.updateLink = function updateLink(link, oldText, newText, start, end) {
    var oldLink = link.getLinkUrl(start);

    if(0 > oldLink.indexOf(oldText)) return false;

    var newLink = oldLink.replace(new RegExp(oldText, 'g'), newText);
    link.setLinkUrl(start || 0, (end || oldLink.length), newLink);
    log(true, "Updating Link: ", oldLink, newLink, start, end, updatedLinkText(link, start) );

    return { old: oldLink, "new": newLink, getText: updatedLinkText(link, start) };
  };

  // moving this reused block out to 'private' fn
  var updateLinkResult = function(text, oldText, newText, link, urls, sidebar, updateResult) {
    // and may as well update the link while we're here
    if(false !== (updateResult = P.updateLink(text, oldText, newText, link.start, link.end))) {
       sidebar.append('<li>' + updateResult['old'] + ' &rarr; ' + updateResult['new'] + ' at ' + updateResult['getText']() + '</li>'); 
    }

    urls.push(link.url); // so multiple links get added to list
  };

  P.updateLinksMenu = function() {
    // https://developers.google.com/apps-script/reference/base/prompt-response
    var oldText = ui.prompt('Old link text to replace').getResponseText();
    var newText = ui.prompt('New link text to replace with').getResponseText();

    log('Replacing: ' + oldText + ', ' + newText);
    var sidebar = gDocUiHelper.createSidebar('Update All Links', '<h3>Replacing</h3><p><code>' + oldText + '</code> &rarr; <code>' + newText + '</code></p><hr /><ol>');

    // current doc available to script
    var doc = DocumentApp.getActiveDocument().getBody();//.getActiveSection();

    // Search until a link is found
    var links = P.findAllElementsFor(doc, function(text) {
      var i = -1, n = text.getText().length, link = false, url, urls = [], updateResult;

      // note: the following only gets the FIRST link in the text -- while(i < n && !(url = text.getLinkUrl(i++)));

      // scan the text element for links
      while(++i < n) {

        // getLinkUrl will continue to get a link while INSIDE the stupid link, so only do this once
        if(url = text.getLinkUrl(i)) {
          if(false === link) {
            link = { start: i, end: -1, url: url };
            // log(true, 'Type: ' + text.getType(), 'Link: ' + url, function() { return 'Text: ' + text.getText().substring(i,100) + ((n-i) > 100 ? '...' : '')});
          }
          else {
            link.end = i; // keep updating the end position until we leave
          }
        }
        // just left the link -- reset link tracking
        else if(false !== link) {
          // and may as well update the link while we're here
          updateLinkResult(text, oldText, newText, link, urls, sidebar);
          link = false; // reset "counter"
        }

      }

      // once we've reached the end of the text, must also check to see if the last thing we found was a link
      if(false !== link) updateLinkResult(text, oldText, newText, link, urls, sidebar);

      return urls;
    });

    sidebar.append('</ol><p><strong>' + links.length + ' links reviewed</strong></p>');
    gDocUiHelper.attachSidebar(sidebar);

    log(links);
  };

  P.findAllElementsFor = function(el, test) {
    // generic utility function to recursively find all elements; heavily based on https://stackoverflow.com/a/18731628/1037948

    var results = [], searchResult = null, i, result;
    // https://developers.google.com/apps-script/reference/document/body#findElement(ElementType)
    while (searchResult = el.findElement(DocumentApp.ElementType.TEXT, searchResult)) {
      var t = searchResult.getElement().editAsText(); // .asParagraph()

      // check to add to list
      if(test && (result = test(t))) {
        if( _.isArray(result) ) results = results.concat(result); // could be big? http://jsperf.com/self-concatenation/
        else results.push(result);
      }
    }
    // recurse children if not plain text item
    if(el.getType() !== DocumentApp.ElementType.TEXT) {
      i = el.getNumChildren();

      var result;
      while(--i > 0) {
        result = P.findAllElementsFor(el.getChild(i));
        if(result && result.length > 0) results = results.concat(result);
      }
    }

    return results;
  };

  return P;  
})({});

// really? it can't handle object properties?
function gDocsUpdateLinksMenu() {
  gDocsHelper.updateLinksMenu();
}

gDocUiHelper.addMenu('Zaus', [ ['Update links', 'gDocsUpdateLinksMenu'] ]);

// #endregion --------------------- Utilities -----------------------------

また、完全を期すために、メニューやサイドバーなどを作成するための「追加の」ユーティリティ クラスを以下に含めています。

var log = function() {
  // return false;

  var args = Array.prototype.slice.call(arguments);

  // allowing functions delegates execution so we can save some non-debug cycles if code left in?

  if(args[0] === true) Logger.log(_.map(args, function(v) { return _.isFunction(v) ? v() : v; }).join('; '));
  else
    _.each(args, function(v) {
      Logger.log(_.isFunction(v) ? v() : v);
    });
}

// #region --------------------- Menu -----------------------------

var gDocUiHelper = (function(P, un) {

  P.addMenuToSheet = function addMenu(spreadsheet, title, items) {
    var menu = ui.createMenu(title);
    // make sure menu items are correct format
    _.each(items, function(v,k) {
      var err = [];

      // provided in format [ [name, fn],... ] instead
      if( _.isArray(v) ) {
        if ( v.length === 2 ) {
          menu.addItem(v[0], v[1]);
        }
        else {
          err.push('Menu item ' + k + ' missing name or function: ' + v.join(';'))
        }
      }
      else {
        if( !v.name ) err.push('Menu item ' + k + ' lacks name');
        if( !v.functionName ) err.push('Menu item ' + k + ' lacks function');

        if(!err.length) menu.addItem(v.name, v.functionName);
      }

      if(err.length) {
        log(err);
        ui.alert(err.join('; '));
      }

    });

    menu.addToUi();
  };

  // list of things to hook into
  var initializers = {};

  P.addMenu = function(menuTitle, menuItems) {
    if(initializers[menuTitle] === un) {
      initializers[menuTitle] = [];
    }
    initializers[menuTitle] = initializers[menuTitle].concat(menuItems);
  };

  P.createSidebar = function(title, content, options) {
    var sidebar = HtmlService
    .createHtmlOutput()
    .setTitle(title)
    .setWidth( (options && options.width) ? width : 350 /* pixels */);

    sidebar.append(content);

    if(options && options.on) DocumentApp.getUi().showSidebar(sidebar);
    // else { sidebar.attach = function() { DocumentApp.getUi().showSidebar(this); }; } // should really attach to prototype...

    return sidebar;
  };

  P.attachSidebar = function(sidebar) {
    DocumentApp.getUi().showSidebar(sidebar);
  };


  P.onOpen = function() {
    var spreadsheet = SpreadsheetApp.getActive();
    log(initializers);
    _.each(initializers, function(v,k) {
      P.addMenuToSheet(spreadsheet, k, v);
    });
  };

  return P;
})({});

// #endregion --------------------- Menu -----------------------------

/**
 * A special function that runs when the spreadsheet is open, used to add a
 * custom menu to the spreadsheet.
 */
function onOpen() {
  gDocUiHelper.onOpen();
}
于 2013-09-13T21:11:10.833 に答える
-1

そうです...検索と置換はここでは適用できません。setLinkUrl() を使用し ます https://developers.google.com/apps-script/reference/document/container-element#setLinkUrl(String)

基本的に、要素を再帰的に反復処理する必要があり (要素には要素を含めることができます)、それぞれに対して getLinkUrl() を使用して oldText を取得する必要があります null でない場合は、 setLinkUrl(newText) .... 表示されたテキストは変更されません。

于 2013-09-10T21:53:23.477 に答える
-1

以下は、スクリプトなしで同じ目標を達成するための簡単で汚い方法です。

  1. Google ドキュメントから、ドキュメントを RTF 形式で保存します。
  2. 選択したエディターで、RTF ファイル内のリンクを編集します (私の場合、すべてのハイパーリンクを変更したかったので、Emacs とregexp-replaceを使用しました)。完了したら、ファイルを保存します。
  3. 新しい Google ドキュメントを作成し、メニューから [ファイル] > [開く] を選択して RTF ファイルを開きます。ドキュメントは、編集した RTF ファイルを適切な Google ドキュメントに変換し、すべてのフォーマットを復元します。

Google ドキュメントの RTF 形式は非常に完成度が高く、往復を行っても忠実度が失われることはありません。ドキュメントに関するすべてのハイパーリンク、書式設定、その他すべてを簡単な形式で完全に公開できるという利点があります。正規表現ツールを編集して適用します。

于 2019-12-19T17:58:16.727 に答える