├── .ruby-version ├── lib ├── abbreviato │ ├── version.rb │ ├── abbreviato.rb │ └── truncated_sax_document.rb └── abbreviato.rb ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── CODEOWNERS └── workflows │ ├── codeql.yaml │ ├── publish.yml │ ├── security.yml │ └── actions.yml ├── .brakeman.ignore ├── .gitignore ├── Gemfile ├── spec ├── spec_helper.rb ├── support │ └── spec_helpers │ │ └── abbreviato_macros.rb ├── fixtures │ ├── cdata_example_truncated.html │ ├── cdata_example.html │ ├── real_world_example_truncated.html │ └── real_world_example.html └── abbreviato │ └── abbreviato_spec.rb ├── abbreviato.gemspec ├── Rakefile ├── LICENSE.txt ├── CONTRIBUTING.md ├── CHANGELOG.md ├── README.md ├── .rubocop.yml └── Gemfile.lock /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.7 2 | -------------------------------------------------------------------------------- /lib/abbreviato/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Abbreviato 4 | VERSION = '3.3.0' 5 | end 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | -------------------------------------------------------------------------------- /.brakeman.ignore: -------------------------------------------------------------------------------- 1 | { 2 | "ignored_warnings": [ 3 | ], 4 | "updated": "2016-09-16 16:12:16 -0700", 5 | "brakeman_version": "4.0.1" 6 | } 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS file 2 | # This file defines who should review code changes in this repository. 3 | 4 | * @zendesk/strong-bad 5 | -------------------------------------------------------------------------------- /lib/abbreviato.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dir["#{File.dirname(__FILE__)}/abbreviato/**/*.rb"].each do |file| 4 | require file 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .byebug_history 3 | .config 4 | .idea/ 5 | *.gem 6 | *.rbc 7 | coverage 8 | doc/ 9 | InstalledFiles 10 | lib/bundler/man 11 | pkg 12 | rdoc 13 | spec/reports 14 | test/tmp 15 | test/version_tmp 16 | tmp 17 | vendor 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'awesome_print' 6 | gem 'benchmark-memory' 7 | gem 'brakeman' 8 | gem 'bundler-audit' 9 | gem 'byebug' 10 | gem 'flay' 11 | gem 'nokogiri', '~> 1.18' 12 | gem 'rake' 13 | gem 'rspec' 14 | gem 'rspec-benchmark' 15 | gem 'rubocop', '~> 1.72' 16 | gem 'rubocop-rake' 17 | gem 'rubocop-rspec' 18 | gem 'thor', '>= 1.4.0' 19 | 20 | gemspec 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | name: "CodeQL public repository scanning" 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | schedule: 7 | - cron: "0 0 * * *" 8 | pull_request_target: 9 | types: [opened, synchronize, reopened] 10 | workflow_dispatch: 11 | permissions: 12 | contents: read 13 | security-events: write 14 | actions: read 15 | packages: read 16 | jobs: 17 | trigger-codeql: 18 | uses: zendesk/prodsec-code-scanning/.github/workflows/codeql_advanced_shared.yml@production 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to RubyGems.org 2 | on: 3 | push: 4 | branches: main 5 | paths: lib/abbreviato/version.rb 6 | workflow_dispatch: 7 | permissions: 8 | contents: read 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | environment: rubygems-publish 13 | if: github.repository_owner == 'zendesk' 14 | permissions: 15 | id-token: write 16 | contents: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | bundler-cache: false 23 | - name: Install dependencies 24 | run: bundle install 25 | - uses: rubygems/release-gem@v1 26 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security 2 | on: [push] 3 | permissions: 4 | contents: read 5 | jobs: 6 | main: 7 | name: bundle-audit 8 | runs-on: ubuntu-latest 9 | env: 10 | RAILS_ENV: test 11 | steps: 12 | - uses: zendesk/checkout@v3 13 | - uses: zendesk/setup-ruby@v1 14 | with: 15 | ruby-version: 3.3.7 16 | 17 | - name: Gem install 18 | run: | 19 | set -eu -o pipefail 20 | gem install bundler-audit 21 | 22 | - run: | 23 | set -eu -o pipefail 24 | 25 | IGNORED="" 26 | 27 | if [ -n "$IGNORED" ]; then 28 | echo "::warning:: Ignored vulnerabilities: $IGNORED" 29 | bundle-audit check --update --gemfile-lock Gemfile.lock --ignore $IGNORED 30 | else 31 | bundle-audit check --update --gemfile-lock Gemfile.lock 32 | fi 33 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 4 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 5 | 6 | require 'rubygems' 7 | require 'bundler' 8 | require 'nokogiri' 9 | require 'rspec-benchmark' 10 | require 'benchmark/memory' 11 | 12 | Bundler.setup 13 | Bundler.require 14 | 15 | # Requires supporting files with custom matchers and macros, etc, 16 | # in ./support/ and its subdirectories. 17 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 18 | 19 | RSpec.configure do |config| 20 | config.extend AbbreviatoMacros 21 | config.include RSpec::Benchmark::Matchers 22 | end 23 | 24 | RSpec::Matchers.define :be_valid_html do 25 | match do |actual| 26 | # Fires exception 27 | Nokogiri::HTML("#{actual}") 28 | true 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/support/spec_helpers/abbreviato_macros.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AbbreviatoMacros 4 | def test_truncation(example_description, should_truncate, options) 5 | it "truncates #{example_description}" do 6 | expected_options = Abbreviato::DEFAULT_OPTIONS.merge(options[:with]) 7 | result, truncated = Abbreviato.truncate(options[:source], expected_options) 8 | expect(result).to eq options[:expected] 9 | expect(result.bytesize).to be <= expected_options[:max_length] 10 | expect(result).to be_valid_html 11 | expect(truncated).to eq should_truncate 12 | end 13 | end 14 | 15 | def it_truncates(example_description, options) 16 | test_truncation(example_description, true, options) 17 | end 18 | 19 | def it_does_not_truncate(example_description, options) 20 | test_truncation(example_description, false, options) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /abbreviato.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.push File.expand_path('lib', __dir__) 4 | 5 | # Maintain your gem's version: 6 | require 'abbreviato/version' 7 | 8 | Gem::Specification.new do |s| 9 | s.name = 'abbreviato' 10 | s.version = Abbreviato::VERSION 11 | 12 | s.authors = ['Jorge Manrubia'] 13 | s.description = 'Truncate HTML to a specific bytesize, while keeping valid markup' 14 | s.email = 'jorge.manrubia@gmail.com' 15 | s.extra_rdoc_files = [ 16 | 'LICENSE.txt', 17 | 'README.md' 18 | ] 19 | s.files = Dir['{app,config,db,lib}/**/*', 'LICENSE.txt', 'Rakefile', 'README.rdoc'] 20 | s.homepage = 'https://github.com/zendesk/abbreviato' 21 | s.licenses = ['MIT'] 22 | s.require_paths = ['lib'] 23 | s.summary = 'A tool for efficiently truncating HTML strings to a specific bytesize' 24 | 25 | s.required_ruby_version = '>= 3.2' 26 | 27 | s.add_dependency 'htmlentities', '~> 4.3.4' 28 | s.add_dependency 'nokogiri', '~> 1.18' 29 | end 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'bundler/gem_tasks' 5 | 6 | require 'rspec/core/rake_task' 7 | RSpec::Core::RakeTask.new(:spec) 8 | 9 | if %w[development test].include?(ENV['RAILS_ENV'] ||= 'development') 10 | require 'bundler/audit/task' 11 | Bundler::Audit::Task.new 12 | 13 | desc 'Analyze for code duplication (large, identical syntax trees) with fuzzy matching.' 14 | task :flay do 15 | require 'flay' 16 | flay = Flay.run(%w[bin config lib script]) 17 | flay.report 18 | 19 | threshold = 0 20 | raise "Flay total too high! #{flay.total} > #{threshold}" if flay.total > threshold 21 | end 22 | 23 | require 'rubocop/rake_task' 24 | RuboCop::RakeTask.new 25 | 26 | desc 'Analyze security vulnerabilities with brakeman' 27 | task :brakeman do 28 | `brakeman --exit-on-warn --exit-on-err --format plain --ensure-latest --table-width 999 --force-scan lib --ignore-config .brakeman.ignore` 29 | end 30 | 31 | desc 'Run all linters' 32 | task lint: %w[rubocop flay brakeman] 33 | end 34 | 35 | task default: :spec 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Original work Copyright (c) 2011 Jorge Manrubia 2 | Modified work Copyright (c) 2017 Zendesk, Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push] 3 | permissions: 4 | contents: read 5 | jobs: 6 | specs: 7 | name: specs 8 | runs-on: ubuntu-latest 9 | env: 10 | RAILS_ENV: test 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | ruby: 15 | - '3.2' 16 | - '3.3' 17 | steps: 18 | - uses: zendesk/checkout@v3 19 | - uses: zendesk/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby }} 22 | bundler-cache: true 23 | - name: Run specs on Ruby ${{ matrix.ruby }} 24 | run: bundle exec rake spec 25 | linter: 26 | name: linter 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: zendesk/checkout@v3 30 | - uses: zendesk/setup-ruby@v1 31 | with: 32 | bundler-cache: true 33 | - run: bundle exec rake lint 34 | specs_successful: 35 | name: Specs passing? 36 | needs: specs 37 | if: always() 38 | runs-on: ubuntu-latest 39 | steps: 40 | - run: | 41 | if ${{ needs.specs.result == 'success' }} 42 | then 43 | echo "All specs passed" 44 | else 45 | echo "Some specs failed" 46 | false 47 | fi 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Pull requests are welcome on GitHub at https://github.com/zendesk/abbreviato 4 | 5 | ## Pull Requests 6 | 7 | - You need at least one review approval before merging. 8 | - Always include specs, and please make sure our CI is green. 9 | - Send GitHub's review request if you need specific reviewers. 10 | - Assign yourself in the PRs. 11 | - Keep your branch up-to-date with the default branch. 12 | 13 | ### Releasing a new version 14 | A new version is published to RubyGems.org every time a change to `version.rb` is pushed to the `main` branch. 15 | In short, follow these steps: 16 | 1. Update `version.rb`, 17 | 2. run `bundle lock` to update `Gemfile.lock`, 18 | 3. merge this change into `main`, and 19 | 4. look at [the action](https://github.com/zendesk/abbreviato/actions/workflows/publish.yml) for output. 20 | 21 | To create a pre-release from a non-main branch: 22 | 1. change the version in `version.rb` to something like `1.2.0.pre.1` or `2.0.0.beta.2`, 23 | 2. push this change to your branch, 24 | 3. go to [Actions → “Publish to RubyGems.org” on GitHub](https://github.com/zendesk/abbreviato/actions/workflows/publish.yml), 25 | 4. click the “Run workflow” button, 26 | 5. pick your branch from a dropdown. 27 | 28 | ## Coding conventions 29 | 30 | - Ruby styleguide: https://github.com/bbatsov/ruby-style-guide 31 | - Specs styleguide: http://betterspecs.org/ 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | * Drops support for Ruby 3.1 10 | 11 | ## [3.3.0] - 2025-10-15 12 | 13 | * #81: Missing release commit 14 | * #83: Bump nokogiri and json gems to resolve multiple CVEs 15 | * #84: Switch to trusted publishing workflow and run it on version.rb changes 16 | * #85: Bump nokogiri and thor to address multiple CVEs 17 | * #86: Adding CodeQL Workflow 18 | * #87: Add workflow permissions 19 | 20 | ## [3.2.0] - 2025-02-25 21 | 22 | * Drops support for Ruby 3.0 23 | * Bumps nokogiri to 1.18.3 24 | * Bumps rubocop to 1.73 to ensure we have support for ruby 3.3 25 | * Bumps ruby to 3.3.7 26 | 27 | ## [3.1.4] - 2025-01-23 28 | 29 | * No/OP 30 | 31 | ## [3.1.3] - 2025-01-23 32 | 33 | * No/OP 34 | 35 | ## [3.1.2] - 2025-01-23 36 | 37 | * Set nokogiri gem to v1.17.2 38 | * Updates rexml from 3.2.8 to 3.3.3 39 | 40 | ## [3.1.1] - 2024-08-13 41 | 42 | * Bump rexml from 3.2.8 to 3.3.3. 43 | 44 | ## [3.1.0] - 2024-08-13 45 | 46 | * Bump the bundler group across 1 directory with 2 updates. 47 | 48 | ## [3.0.0] - 2024-03-06 49 | 50 | * Drops support for Ruby 2.7 51 | * Adds tests on Ruby 3.3 52 | 53 | ## [2.0.0] - 2023-06-14 54 | 55 | * Adds support for Ruby 3.0, 3.1, and 3.2 56 | * Drops support for Ruby 2.6 57 | * Invalid HTML with unpaired opening brackets is now truncated to a non-HTML string, e.g., `"<

hello there

"` will be truncated to `"<

"`. Previously, it was truncated to a valid HTML string `"

"`. If your inputs can be invalid HTML strings, you should check them before abbreviating (for example, using Nokogiri). 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # abbreviato 2 | 3 | [![Continuous Integration](https://github.com/zendesk/abbreviato/actions/workflows/actions.yml/badge.svg)](https://github.com/zendesk/abbreviato/actions/workflows/actions.yml) 4 | [![Security](https://github.com/zendesk/abbreviato/actions/workflows/security.yml/badge.svg)](https://github.com/zendesk/abbreviato/actions/workflows/security.yml) 5 | [![Gem Version](https://img.shields.io/gem/v/abbreviato.svg)](https://rubygems.org/gems/abbreviato) 6 | 7 | *abbreviato* is a Ruby library for truncating HTML strings keeping the markup valid. It is a fork of [jorgemanrubia/truncato](https://github.com/jorgemanrubia/truncato) but focused on truncating to a bytesize, not on a per-character basis. 8 | 9 | ## Installing 10 | 11 | In your `Gemfile` 12 | 13 | ```ruby 14 | gem 'abbreviato' 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```ruby 20 | truncated_string, was_truncated = Abbreviato.truncate("

some text

", max_length: 4) 21 | # => ["

s...

", true] 22 | ``` 23 | 24 | The configuration options are: 25 | 26 | * `max_length`: The size, in bytes, to truncate (`30` by default) 27 | * `tail`: The string to append when the truncation occurs ('…' by default). 28 | * `fragment`: Indicates whether the document to be truncated is an HTML fragment or an entire document (with `HTML`, `HEAD` & `BODY` tags). Setting to true prevents automatic 29 | addition of these tags if they are missing. Defaults to `true`. 30 | 31 | ## Performance 32 | 33 | Abbreviato was designed with performance in mind. Its main motivation was that existing libs couldn't truncate a multiple-MB document into a few-KB one in a reasonable time. It uses the [Nokogiri](http://nokogiri.org/) SAX parser. 34 | 35 | ## Running the tests 36 | 37 | ```ruby 38 | bundle exec rake 39 | ``` 40 | 41 | ## Running a single test 42 | 43 | ```ruby 44 | rspec spec/abbreviato/abbreviato_spec.rb 45 | rspec spec/abbreviato/abbreviato_spec.rb:357 46 | ``` 47 | 48 | ## Running all checks 49 | 50 | ```ruby 51 | bundle exec rake spec 52 | ``` 53 | 54 | ## Contribute 55 | 56 | Follow our [contribution guidelines](CONTRIBUTING.md). 57 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/bbatsov/rubocop/blob/master/config/enabled.yml 2 | 3 | require: 4 | - rubocop-rake 5 | - rubocop-rspec 6 | 7 | AllCops: 8 | NewCops: enable 9 | ExtraDetails: true 10 | DisplayCopNames: true 11 | DisplayStyleGuide: true 12 | Include: 13 | - '*.gemspec' 14 | - 'Gemfile' 15 | - 'lib/**/*.rb' 16 | - 'Rakefile' 17 | - 'spec/**/*.rb' 18 | Exclude: 19 | - "**/vendor/**/*" 20 | 21 | # Missing top-level module documentation comment. 22 | Style/Documentation: 23 | Enabled: false 24 | 25 | # Assignment Branch Condition size for truncate is too high. (http://c2.com/cgi/wiki?AbcMetric, https://en.wikipedia.org/wiki/ABC_Software_Metric) 26 | Metrics/AbcSize: 27 | Enabled: false 28 | 29 | # Method has too many lines. [10] (https://rubystyle.guide#short-methods) 30 | Metrics/MethodLength: 31 | Enabled: false 32 | 33 | # Block has too many lines. [25] 34 | Metrics/BlockLength: 35 | Enabled: false 36 | 37 | # Class has too many lines. [100] 38 | Metrics/ClassLength: 39 | Enabled: false 40 | 41 | # Cyclomatic complexity for truncate is too high. [7] 42 | Metrics/CyclomaticComplexity: 43 | Enabled: false 44 | 45 | # Perceived complexity for truncate is too high. [8] 46 | Metrics/PerceivedComplexity: 47 | Enabled: false 48 | 49 | # Do not define constants this way within a block. (https://rubystyle.guide#no-constant-definition-in-block) 50 | Lint/ConstantDefinitionInBlock: 51 | Enabled: false 52 | 53 | # Line is too long. [120] (https://rubystyle.guide#max-line-length) 54 | Layout/LineLength: 55 | Enabled: false 56 | 57 | # Example has too many expectations [1]. (https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MultipleExpectations) 58 | RSpec/MultipleExpectations: 59 | Enabled: false 60 | 61 | # Example has too many lines. [5] (https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/ExampleLength) 62 | RSpec/ExampleLength: 63 | Enabled: false 64 | 65 | # Empty example group detected. (https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/EmptyExampleGroup) 66 | RSpec/EmptyExampleGroup: 67 | Enabled: false 68 | 69 | Gemspec/RequireMFA: 70 | Enabled: false 71 | 72 | Gemspec/RequiredRubyVersion: 73 | Enabled: false 74 | -------------------------------------------------------------------------------- /lib/abbreviato/abbreviato.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Abbreviato 4 | DEFAULT_OPTIONS = { 5 | max_length: 30, 6 | tail: '…', 7 | fragment: true 8 | }.freeze 9 | 10 | # Truncates the source XML string and returns the truncated XML and a boolean flag indicating 11 | # whether any truncation took place. It will keep a valid XML structure 12 | # and insert a _tail_ text indicating the position where content was removed (...). 13 | # 14 | # @param [String] source the XML source to truncate 15 | # @param [Hash] user_options truncation options 16 | # @option user_options [Integer] :max_length Maximum length 17 | # @option user_options [Boolean] :truncate_incomplete_row Indicates whether or 18 | # not to truncate the last row in a table if truncation due to max_length 19 | # occurs in the middle of a row. 20 | # @option user_options [String] :tail Text to append when the truncation happens 21 | # @option user_options [Boolean] :fragment Indicates whether the document to be truncated is an HTML fragment 22 | # or an entire document (with `HTML`, `HEAD` & `BODY` tags). Setting to true prevents automatic addition of 23 | # these tags if they are missing. Defaults to `true`. 24 | # @return [[String] the truncated string, [boolean] whether the string was truncated] 25 | def self.truncate(source = '', user_options = {}, encoding = 'UTF-8') 26 | return [nil, false] if source.nil? 27 | return ['', false] if source.blank? 28 | 29 | truncated_sax_document = TruncatedSaxDocument.new(DEFAULT_OPTIONS.merge(user_options)) 30 | parser = Nokogiri::HTML::SAX::Parser.new(truncated_sax_document, encoding) 31 | parser.parse(source) { |context| context.replace_entities = false } 32 | 33 | if truncated_sax_document.truncated && user_options[:truncate_incomplete_row] 34 | parsed_results = [truncated_sax_document.truncated_string.strip, truncated_sax_document.truncated] 35 | 36 | html_fragment = Nokogiri::HTML.fragment(truncated_sax_document.truncated_string.strip) 37 | return parsed_results if html_fragment.nil? 38 | 39 | last_table_in_doc = html_fragment.xpath('.//table').last 40 | return parsed_results unless last_table_in_doc 41 | 42 | first_row = last_table_in_doc.xpath('.//tr').first 43 | return parsed_results unless first_row 44 | 45 | cols_in_first_row = first_row.xpath('.//td').length 46 | return parsed_results unless cols_in_first_row.positive? 47 | 48 | last_table_in_doc.xpath('.//tr').each do |row| 49 | row.remove if row.xpath('.//td').length != cols_in_first_row 50 | end 51 | 52 | return [html_fragment.to_html, truncated_sax_document.truncated] 53 | end 54 | 55 | [truncated_sax_document.truncated_string.strip, truncated_sax_document.truncated] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | abbreviato (3.3.0) 5 | htmlentities (~> 4.3.4) 6 | nokogiri (~> 1.18) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | ast (2.4.2) 12 | awesome_print (1.8.0) 13 | benchmark-malloc (0.1.0) 14 | benchmark-memory (0.1.2) 15 | memory_profiler (~> 0.9) 16 | benchmark-perf (0.5.0) 17 | benchmark-trend (0.3.0) 18 | brakeman (5.0.1) 19 | bundler-audit (0.9.2) 20 | bundler (>= 1.2.0, < 3) 21 | thor (~> 1.0) 22 | byebug (11.1.1) 23 | diff-lcs (1.3) 24 | erubi (1.12.0) 25 | flay (2.13.2) 26 | erubi (~> 1.10) 27 | path_expander (~> 1.0) 28 | ruby_parser (~> 3.0) 29 | sexp_processor (~> 4.0) 30 | htmlentities (4.3.4) 31 | json (2.10.2) 32 | language_server-protocol (3.17.0.4) 33 | lint_roller (1.1.0) 34 | memory_profiler (0.9.14) 35 | mini_portile2 (2.8.9) 36 | nokogiri (1.18.9) 37 | mini_portile2 (~> 2.8.2) 38 | racc (~> 1.4) 39 | parallel (1.26.3) 40 | parser (3.3.7.1) 41 | ast (~> 2.4.1) 42 | racc 43 | path_expander (1.1.0) 44 | racc (1.8.1) 45 | rainbow (3.1.1) 46 | rake (13.0.1) 47 | regexp_parser (2.10.0) 48 | rspec (3.5.0) 49 | rspec-core (~> 3.5.0) 50 | rspec-expectations (~> 3.5.0) 51 | rspec-mocks (~> 3.5.0) 52 | rspec-benchmark (0.5.1) 53 | benchmark-malloc (~> 0.1.0) 54 | benchmark-perf (~> 0.5.0) 55 | benchmark-trend (~> 0.3.0) 56 | rspec (>= 3.0.0, < 4.0.0) 57 | rspec-core (3.5.4) 58 | rspec-support (~> 3.5.0) 59 | rspec-expectations (3.5.0) 60 | diff-lcs (>= 1.2.0, < 2.0) 61 | rspec-support (~> 3.5.0) 62 | rspec-mocks (3.5.0) 63 | diff-lcs (>= 1.2.0, < 2.0) 64 | rspec-support (~> 3.5.0) 65 | rspec-support (3.5.0) 66 | rubocop (1.72.2) 67 | json (~> 2.3) 68 | language_server-protocol (~> 3.17.0.2) 69 | lint_roller (~> 1.1.0) 70 | parallel (~> 1.10) 71 | parser (>= 3.3.0.2) 72 | rainbow (>= 2.2.2, < 4.0) 73 | regexp_parser (>= 2.9.3, < 3.0) 74 | rubocop-ast (>= 1.38.0, < 2.0) 75 | ruby-progressbar (~> 1.7) 76 | unicode-display_width (>= 2.4.0, < 4.0) 77 | rubocop-ast (1.38.0) 78 | parser (>= 3.3.1.0) 79 | rubocop-rake (0.5.1) 80 | rubocop 81 | rubocop-rspec (2.3.0) 82 | rubocop (~> 1.0) 83 | rubocop-ast (>= 1.1.0) 84 | ruby-progressbar (1.13.0) 85 | ruby_parser (3.15.1) 86 | sexp_processor (~> 4.9) 87 | sexp_processor (4.13.0) 88 | thor (1.4.0) 89 | unicode-display_width (3.1.4) 90 | unicode-emoji (~> 4.0, >= 4.0.4) 91 | unicode-emoji (4.0.4) 92 | 93 | PLATFORMS 94 | ruby 95 | 96 | DEPENDENCIES 97 | abbreviato! 98 | awesome_print 99 | benchmark-memory 100 | brakeman 101 | bundler-audit 102 | byebug 103 | flay 104 | nokogiri (~> 1.18) 105 | rake 106 | rspec 107 | rspec-benchmark 108 | rubocop (~> 1.72) 109 | rubocop-rake 110 | rubocop-rspec 111 | thor (>= 1.4.0) 112 | 113 | BUNDLED WITH 114 | 2.5.6 115 | -------------------------------------------------------------------------------- /spec/fixtures/cdata_example_truncated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /spec/fixtures/cdata_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /lib/abbreviato/truncated_sax_document.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | 3 | require 'nokogiri' 4 | require 'htmlentities' 5 | 6 | class TruncatedSaxDocument < Nokogiri::XML::SAX::Document 7 | IGNORABLE_TAGS = %w[html head body].freeze 8 | 9 | # These don't have to be closed (which also impacts ongoing length calculations) 10 | # http://www.456bereastreet.com/archive/201005/void_empty_elements_and_self-closing_start_tags_in_html/ 11 | VOID_TAGS = %w[area base br col command hr img input keygen link meta param source wbr].freeze 12 | 13 | attr_reader :truncated_string, 14 | :max_length, 15 | :tail, 16 | :ignored_levels, 17 | :truncated 18 | 19 | # FIXME: Call super to initialize state of the parent class. 20 | def initialize(options) # rubocop:disable Lint/MissingSuper 21 | @html_coder = HTMLEntities.new 22 | 23 | @max_length = options[:max_length] 24 | @tail = options[:tail] || '' 25 | @fragment_mode = options[:fragment] 26 | 27 | @truncated_string = '' 28 | @closing_tags = [] 29 | @estimated_length = 0 30 | @ignored_levels = 0 31 | @truncated = false 32 | end 33 | 34 | # This method is called when the parser encounters an open tag 35 | def start_element(name, attributes) 36 | if max_length_reached? || ignorable_tag?(name) 37 | @truncated = true if max_length_reached? 38 | return 39 | end 40 | 41 | # If already in ignore mode, go in deeper 42 | if ignore_mode? 43 | enter_ignored_level(name) 44 | return 45 | end 46 | 47 | string_to_add = opening_tag(name, attributes) 48 | 49 | # Abort if there is not enough space to add the combined opening tag and (potentially) the closing tag 50 | length_of_tags = overridden_tag_length(name, string_to_add) 51 | if length_of_tags > remaining_length 52 | @truncated = true 53 | enter_ignored_level(name) 54 | return 55 | end 56 | 57 | # Save the tag so we can push it on at the end 58 | @closing_tags.push name unless single_tag_element?(name) 59 | 60 | append_to_truncated_string(string_to_add, length_of_tags) 61 | end 62 | 63 | # This method is called when the parser encounters characters between tags 64 | def characters(decoded_string) 65 | if max_length_reached? || ignore_mode? 66 | @truncated = true 67 | return 68 | end 69 | 70 | # Use encoded length, so > counts as 4 bytes, not 1 (which is what '>' would give) 71 | encoded_string = @html_coder.encode(decoded_string, :named) 72 | string_to_append = if encoded_string.bytesize > remaining_length 73 | # This is the line which prevents HTML entities getting truncated - treat them as a single char 74 | str = truncate_string(decoded_string) 75 | str << tail if remaining_length - str.bytesize >= tail.bytesize 76 | str 77 | else 78 | encoded_string 79 | end 80 | append_to_truncated_string(string_to_append) 81 | end 82 | 83 | # This method is called when the parser encounters a comment 84 | def comment(string) 85 | comment = comment_tag(string) 86 | if comment.bytesize <= remaining_length 87 | append_to_truncated_string(comment) 88 | else 89 | @truncated = true 90 | end 91 | end 92 | 93 | # This method is called when the parser encounters cdata. In practice, this also 94 | # gets called for this style of comment inside an element: 95 | # 96 | # 102 | # 103 | def cdata_block(string) 104 | if string.bytesize <= remaining_length 105 | append_to_truncated_string(string) 106 | else 107 | @truncated = true 108 | end 109 | end 110 | 111 | # This method is called when the parser encounters a closing tag 112 | def end_element(name) 113 | if ignore_mode? 114 | exit_ignored_level(name) 115 | return 116 | end 117 | 118 | # Note that any remaining end tags get added automatically (in `end_document`) as the document is closed 119 | return if max_length_reached? || ignorable_tag?(name) 120 | 121 | # FIXME: Style/GuardClause: Use a guard clause (return if single_tag_element?(name)) instead of wrapping the code inside a conditional expression. (https://rubystyle.guide#no-nested-conditionals) 122 | unless single_tag_element?(name) # rubocop:disable Style/GuardClause 123 | @closing_tags.pop 124 | # Don't count the length when closing a tag - it was accommodated when 125 | # the tag was opened 126 | append_to_truncated_string(closing_tag(name), 0) 127 | end 128 | end 129 | 130 | def end_document 131 | @closing_tags.reverse_each { |name| append_to_truncated_string(closing_tag(name), 0) } 132 | end 133 | 134 | private 135 | 136 | def opening_tag(name, attributes) 137 | attributes_string = attributes_to_string(attributes) 138 | if single_tag_element? name 139 | "<#{name}#{attributes_string}/>" 140 | else 141 | "<#{name}#{attributes_string}>" 142 | end 143 | end 144 | 145 | def comment_tag(comment) 146 | "" 147 | end 148 | 149 | def closing_tag(name) 150 | "" 151 | end 152 | 153 | def remaining_length 154 | max_length - @estimated_length 155 | end 156 | 157 | def single_tag_element?(name) 158 | VOID_TAGS.include? name 159 | end 160 | 161 | def append_to_truncated_string(string, overridden_length = nil) 162 | @truncated_string << string 163 | @estimated_length += overridden_length || string.bytesize 164 | end 165 | 166 | def attributes_to_string(attributes) 167 | attributes.inject(' ') do |string, attribute| 168 | key, value = attribute 169 | string << "#{key}='#{@html_coder.encode value}' " 170 | end.rstrip 171 | end 172 | 173 | def max_length_reached? 174 | @estimated_length >= max_length 175 | end 176 | 177 | def truncate_string(decoded_string) 178 | @truncated = true 179 | truncate_length = remaining_length - tail.bytesize 180 | truncated_string = '' 181 | decoded_string.chars.each do |char| 182 | encoded_char = @html_coder.encode(char) 183 | break if encoded_char.bytesize > truncate_length 184 | 185 | truncated_string += encoded_char 186 | truncate_length -= encoded_char.bytesize 187 | end 188 | truncated_string.scrub('') 189 | end 190 | 191 | def overridden_tag_length(tag_name, rendered_tag_with_attributes) 192 | # Start with the opening tag 193 | length = rendered_tag_with_attributes.bytesize 194 | 195 | # Add on closing tag if necessary 196 | length += closing_tag(tag_name).bytesize unless single_tag_element?(tag_name) 197 | length 198 | end 199 | 200 | def ignorable_tag?(name) 201 | @fragment_mode && IGNORABLE_TAGS.include?(name.downcase) 202 | end 203 | 204 | def enter_ignored_level(name) 205 | @ignored_levels += 1 unless single_tag_element?(name) 206 | end 207 | 208 | def exit_ignored_level(name) 209 | @ignored_levels -= 1 unless single_tag_element?(name) 210 | end 211 | 212 | def ignore_mode? 213 | @ignored_levels.positive? 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /spec/fixtures/real_world_example_truncated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 147 | 148 | 149 | 150 | John / Pete, 151 | 152 | Good morning. I am back from vacation and know there were questions regarding the quote for coolers and gasket kit while I was out. During my absence, I had Johny Johnson forward 153 | my quote to John for his reference. 154 | 155 | Are there any outstanding questions you have? If not, when do you anticipate the order will be placed? I want to make sure this is pushed through quickly so we meet your 156 | onsite date set forth by Janey Janeson. 157 | 158 | Confirming information again: 159 | 160 | 161 | 162 | 163 | 164 | Qty 165 | 166 | 167 | PN 168 | 169 | 170 | Net Each 171 | 172 | 173 | Total 174 | 175 | 176 | 177 | 178 | 3 179 | 180 | 181 | 218100 182 | 183 | 184 | 2,611.00 185 | 186 | 187 | 7,833.00 188 | 189 | 190 | 191 | 192 | 1 193 | 194 | 195 | 6750346 196 | 197 | 198 | 434.00 199 | 200 | 201 | 434.00 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | Total: 213 | 214 | 215 | 76,267.00 216 | 217 | 218 | 219 | 220 | 221 | 222 | Thank you and regards, 223 | 224 | Katy Katyson 225 | Order Management 226 | Helicopter Refitting and Services 227 | Helicopter And Doggie Grooming Services 228 | 1000 Main Street 229 | San Francisco, CA 94100 USA 230 | #858584 231 | Office: 123-456-7890 232 | E-Fax: 1-123-456-7890 233 | 234 | Email: 235 | linda_lindason@helidogs.com 236 | Website: 237 | www.helidogs.com 238 | Parts Emergencies After Hours: 123-456-7890 239 | 240 | 241 | 242 | 243 | 244 | From: Johntopher@blah.com [mailto:Johntopher@blah.com] 245 | 246 | Sent: Thursday, August 18, 2016 11:59 AM 247 | To: Monkey, Smith 248 | Cc: Lindason, Linda 249 | Subject: RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 Gasket Kits 250 | 251 | Monkey/Linda, 252 | 253 | 254 | Could we please get a firm quote on this one? Reason I ask is Pete wants to make sure he adds this part to his storeroom so the next time they need to order, he can pull this quote for refernce and 255 | order the exact same cooler. 256 | 257 | Thanks, 258 | 259 | john Johnson 260 | 261 | Regional Lead Buyer - Plant Production 262 | 263 | Region Americas Procurement 264 | 265 | LotsOStuff Inc 266 | 267 | 268 | Someplace, USA 269 | 270 | Office: 1 234-567-7890; Fax: 1 234-567-7890 271 | 272 | Mobile: 1 234-567-7890 273 | 274 | Email: 275 | Johntopher@blah.com | www.monkeywrenches.com 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | From: Monkey, Smith Brady_McBunch@iiii.com 284 | 285 | To: Morten.McMortensson@monkeywrenches.com Morten.McMortensson@monkeywrenches.com, 286 | 287 | Cc: Johntopher@blah.com Johntopher@blah.com, 288 | Lindason, Linda linda_lindason@helidogs.com 289 | 290 | Date: 08/16/2016 09:31 AM 291 | 292 | Subject: RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 Gasket Kits 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 10-4 Pete 301 | 302 | 303 | 304 | THANKS 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | Gibb 313 | 314 | 513-383-5019 315 | 316 | 317 | 318 | From: 319 | Morten.McMortensson@monkeywrenches.com [mailto:Morten.McMortensson@monkeywrenches.com] 320 | 321 | Sent: Tuesday, August 16, 2016 9:29 AM 322 | To: Monkey, Smith Brady_McBunch@iiii.com 323 | Cc: Johntopher@blah.com; Lindason, Linda linda_lindason@helidogs.com 324 | Subject: RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 Gasket Kits 325 | 326 | 327 | I will get the PR in today and speak with the procurement folks about getting it expedited to you today. 328 | 329 | 330 | Murph McMurphsson 331 | LotsOStuff LLC 332 | Area Supply Manager, Washington WV 333 | 334 | 1 1st St 335 | 336 | San Francisco, CA 94100 337 | 338 | Office: 1-234-567-8900 339 | 340 | Mobile: 1-234-567-8900 341 | 342 | Fax: 1-234-567-8900 343 | Morten.McMortensson@monkeywrenches.com 344 | 345 | 346 | 347 | 348 | From: Monkey, Smith Brady_McBunch@iiii.com 349 | 350 | To: Lindason, Linda linda_lindason@helidogs.com, 351 | Johntopher@blah.com Johntopher@blah.com, 352 | Morten.McMortensson@monkeywrenches.com Morten.McMortensson@monkeywrenches.com, 353 | 354 | Date: 08/16/2016 09:18 AM 355 | 356 | Subject: RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 Gasket Kits 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | Pete 366 | When do you expect the purchase order to hit? Linda and I will try to allocate the stock for this order asap. THANKS 367 | 368 | 369 | 370 | 371 | Gibb 372 | 373 | 513-383-5019 374 | 375 | 376 | From: Lindason, Linda 377 | 378 | Sent: Monday, August 15, 2016 3:28 PM 379 | To: Johntopher@blah.com; 380 | Morten.McMortensson@monkeywrenches.com 381 | Cc: Monkey, Smith Brady_McBunch@iiii.com 382 | Subject: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 Gasket Kits 383 | 384 | 385 | John / Pete 386 | 387 | The gasket kit is part number 34534563. Net price is 2,434.00. Currently 3 kits in stock. 388 | 389 | 390 | Please advise any questions, thank you. 391 | 392 | 393 | Regards, 394 | 395 | Katy Katyson 396 | 397 | Order Management 398 | 399 | Helicopter Refitting and Services 400 | 401 | Helicopter And Doggie Grooming Services 402 | 403 | 1000 Main Street 404 | 405 | San Francisco, NC 94100 USA 406 | 407 | 408 | Office: 123-456-7890 409 | 410 | E-Fax: 1-123-456-7890 411 | Email: linda_lindason@helidogs.com 412 | 413 | Website: www.helidogs.com 414 | 415 | Parts Emergencies After Hours: 123-456-7890 416 | 417 | 418 | From: 419 | Johntopher@blah.com [mailto:Johntopher@blah.com] 420 | 421 | Sent: Monday, August 15, 2016 3:05 PM 422 | To: Morten.McMortensson@monkeywrenches.com 423 | Cc: Monkey, Smith Brady_McBunch@iiii.com; 424 | Lindason, Linda linda_lindason@helidogs.com 425 | Subject: RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 426 | 427 | Brandon, 428 | 429 | Pete will start working on getting a work order in the system so that we do not lose any coolers that are in stock. We will order the 3. 430 | 431 | 432 | Thanks, 433 | 434 | john Johnson 435 | 436 | Regional Lead Buyer - Plant Production 437 | 438 | Region Americas Procurement 439 | 440 | LotsOStuff Inc 441 | 442 | 443 | Someplace, USA 444 | 445 | Office: 1 234-567-7890; Fax: 1 234-567-7890 446 | 447 | Mobile: 1 234-567-7890 448 | 449 | Email: Johntopher@blah.com | 450 | www.monkeywrenches.com 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | From: Murph McMurphsson/US/LG/LotsOStuffGroup 459 | 460 | To: Monkey, Smith Brady_McBunch@iiii.com, 461 | 462 | Cc: Johntopher@blah.com Johntopher@blah.com, 463 | Lindason, Linda linda_lindason@helidogs.com 464 | 465 | Date: 08/15/2016 03:02 PM 466 | 467 | Subject: RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | I will also need the gasket kits. I will get you the numbers for the kits. 477 | 478 | 479 | Murph McMurphsson 480 | LotsOStuff LLC 481 | Area Supply Manager, Washington WV 482 | 483 | 1 1st St 484 | 485 | San Francisco, CA 94100 486 | 487 | Office: 1-234-567-8900 488 | 489 | Mobile: 1-234-567-8900 490 | 491 | Fax: 1-234-567-8900 492 | Morten.McMortensson@monkeywrenches.com 493 | 494 | 495 | 496 | 497 | 498 | From: Monkey, Smith Brady_McBunch@iiii.com 499 | 500 | To: Johntopher@blah.com 501 | Johntopher@blah.com, Lindason, Linda linda_lindason@helidogs.com, 502 | Morten.McMortensson@monkeywrenches.com Morten.McMortensson@monkeywrenches.com, 503 | 504 | Date: 08/15/2016 02:43 PM 505 | 506 | Subject: RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | Ok John, 517 | 518 | We have 7 of these in stock. Linda I are able to honor that March priceYour cost each = $24,611. 519 | 520 | 521 | Advise what you need. THANKS John 522 | 523 | 524 | 525 | Gibb 526 | 527 | 513-383-5019 528 | 529 | From: 530 | Johntopher@blah.com [mailto:Johntopher@blah.com] 531 | 532 | Sent: Monday, August 15, 2016 1:36 PM 533 | To: Lindason, Linda linda_lindason@helidogs.com; 534 | Morten.McMortensson@monkeywrenches.com 535 | Cc: Monkey, Smith Brady_McBunch@iiii.com; 536 | Murphey, James James.Murphey@iiii.com 537 | Subject: Fw: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 538 | 539 | Linda, 540 | Here is the nameplate on the unit that these coolers are needed for. 541 | 542 | 543 | Pete, 544 | The length is different dependent on the 125 vs. 150. 545 | 546 | 547 | Thanks, 548 | 549 | john Johnson 550 | 551 | Regional Lead Buyer - Plant Production 552 | 553 | Region Americas Procurement 554 | 555 | LotsOStuff Inc 556 | 557 | 558 | Someplace, USA 559 | 560 | Office: 1 234-567-7890; Fax: 1 234-567-7890 561 | 562 | Mobile: 1 234-567-7890 563 | 564 | Email: Johntopher@blah.com | 565 | www.monkeywrenches.com 566 | 567 | 568 | 569 | 570 | ----- Forwarded by Johntopher Rolf/US/LG/LotsOStuffGroup on 08/15/2016 01:34 PM ----- 571 | 572 | 573 | From: Murph McMurphsson/US/LG/LotsOStuffGroup 574 | 575 | To: Johntopher Rolf/US/LG/LotsOStuffGroup@LotsOStuffGroup, 576 | 577 | Date: 08/15/2016 01:33 PM 578 | 579 | Subject: Re: Fw: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | John, 590 | This is the compressor that I need them for. From what I understand the coolers should be the same for a 100, 125 or a 150. I could be wrong. 591 | 592 | 593 | 594 | 595 | Murph McMurphsson 596 | LotsOStuff LLC 597 | Area Supply Manager, Washington WV 598 | 599 | 1 1st St 600 | 601 | San Francisco, CA 94100 602 | 603 | Office: 1-234-567-8900 604 | 605 | Mobile: 1-234-567-8900 606 | 607 | Fax: 1-234-567-8900 608 | Morten.McMortensson@monkeywrenches.com 609 | 610 | 611 | 612 | 613 | From: Johntopher Rolf/US/LG/LotsOStuffGroup 614 | 615 | To: Murph McMurphsson/US/LG/LotsOStuffGroup@LotsOStuffGroup, 616 | 617 | Date: 08/15/2016 01:15 PM 618 | 619 | Subject: Fw: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | Pete, 630 | 631 | IR is getting conflicting information. Originally, you had me quote serial# 4125. Now the updated pic is 4150. I'm not sure which you had CTI quote. What is the correct serial number you have on this unit that you need 3 of? 632 | 633 | 634 | Thanks, 635 | 636 | john Johnson 637 | 638 | Regional Lead Buyer - Plant Production 639 | 640 | Region Americas Procurement 641 | -------------------------------------------------------------------------------- /spec/abbreviato/abbreviato_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'byebug' 5 | require 'awesome_print' 6 | 7 | describe 'Abbreviato' do 8 | before { stub(NBSP: Nokogiri::HTML(' ').text) } 9 | 10 | describe 'normal strings' do 11 | it 'handles nil' do 12 | result, truncated = Abbreviato.truncate(nil) 13 | expect(result).to be_nil 14 | expect(truncated).to be_falsey 15 | end 16 | 17 | it_does_not_truncate 'empty html text with longer length', 18 | with: { max_length: 99 }, 19 | source: '', 20 | expected: '' 21 | it_does_not_truncate 'no html text with longer length', 22 | with: { max_length: 99 }, 23 | source: 'This is some text', 24 | expected: '

This is some text

' 25 | it_truncates 'no html text with shorter length, no tail', 26 | with: { max_length: 12, tail: '' }, 27 | source: 'some text', 28 | expected: '

some

' 29 | it_truncates 'no html text with shorter length, nil tail', 30 | with: { max_length: 12, tail: nil }, 31 | source: 'some text', 32 | expected: '

some

' 33 | it_truncates 'no html text with shorter length, with tail', 34 | with: { max_length: 17 }, 35 | source: 'This is some text', 36 | expected: '

Th…

' 37 | end 38 | 39 | describe 'multi-byte strings' do 40 | # These examples purposely specify a number of bytes which is not divisible by four, to ensure 41 | # characters don't get brokwn up part-way thorugh their multi-byte representation 42 | it_does_not_truncate 'no html text with longer length', 43 | with: { max_length: 99, tail: '...' }, 44 | source: '𠲖𠲖𠲖𠲖𠲖𠲖𠲖𠲖', 45 | expected: '

𠲖𠲖𠲖𠲖𠲖𠲖𠲖𠲖

' 46 | it_truncates 'no html text with shorter length, no tail', 47 | with: { max_length: 11, tail: '' }, 48 | source: '𠝹𠝹𠝹𠝹𠝹𠝹𠝹𠝹', 49 | expected: '

𠝹

' 50 | it_truncates 'no html text with shorter length, with tail', 51 | with: { max_length: 14, tail: '...' }, 52 | source: '𠝹𠝹', 53 | expected: '

𠝹...

' 54 | end 55 | 56 | describe 'multi-byte tail' do 57 | # These examples purposely specify a number of bytes which is not divisible by four, to ensure 58 | # characters don't get brokwn up part-way thorugh their multi-byte representation 59 | it_does_not_truncate 'html text with longer length', 60 | with: { max_length: 99, tail: '𠴕' }, 61 | source: '

𠲖𠲖𠲖𠲖𠲖𠲖𠲖𠲖

', 62 | expected: '

𠲖𠲖𠲖𠲖𠲖𠲖𠲖𠲖

' 63 | it_does_not_truncate 'html text with equal length', 64 | with: { max_length: 15, tail: '𠴕' }, 65 | source: '

𠝹𠝹

', 66 | expected: '

𠝹𠝹

' 67 | it_truncates 'html text with shorter length', 68 | with: { max_length: 15, tail: '𠴕' }, 69 | source: '

𠝹𠝹𠝹𠝹

', 70 | expected: '

𠝹𠴕

' 71 | it_truncates 'html text with shorter length and longer tail', 72 | with: { max_length: 23, tail: '𠴕𠴕𠴕' }, 73 | source: '

𠝹𠝹𠝹𠝹𠝹𠝹

', 74 | expected: '

𠝹𠴕𠴕𠴕

' 75 | end 76 | 77 | describe 'html entity (ellipsis) tail' do 78 | it_truncates 'html text with ellipsis html entity tail', 79 | with: { max_length: 27, tail: '…' }, 80 | source: '

This is some text which will be truncated

', 81 | expected: '

This is some…

' 82 | end 83 | 84 | describe 'html tags structure' do 85 | it_truncates 'html text with tag', 86 | with: { max_length: 14, tail: '...' }, 87 | source: '

some text

', 88 | expected: '

some...

' 89 | 90 | it_truncates 'html text with nested tags (first node)', 91 | with: { max_length: 22, tail: '...' }, 92 | source: '

some text 1

some text 2

', 93 | expected: '

s...

' 94 | 95 | it_truncates 'html text with nested tags (second node)', 96 | with: { max_length: 46, tail: '...' }, 97 | source: '

some text 1

some text 2

', 98 | expected: '

some text 1

some te...

' 99 | 100 | it_truncates 'html text with nested tags (empty contents)', 101 | with: { max_length: 13, tail: '...' }, 102 | source: '

some text 1

some text 2

', 103 | expected: '
' 104 | 105 | it_truncates 'html text with special html entities', 106 | with: { max_length: 15, tail: '...' }, 107 | source: '

>some text

', 108 | expected: '

>s...

' 109 | 110 | it_truncates 'html text with siblings tags', 111 | with: { max_length: 64, tail: '...' }, 112 | source: '
some text 0

some text 1

some text 2

', 113 | expected: '
some text 0

some text 1

som...

' 114 | 115 | it_does_not_truncate 'html with unclosed tags', 116 | with: { max_length: 151, tail: '...' }, 117 | source: '
Hi
there
', 118 | expected: '
Hi
there
' 119 | 120 | it_does_not_truncate 'preserve html entities', 121 | with: { max_length: 99, tail: '...' }, 122 | source: ' ', 123 | expected: ' ' 124 | 125 | it_truncates 'preserves entire html entities (all or nothing)', 126 | with: { max_length: 26, tail: '...' }, # Too small to bring all of   in 127 | source: 'Hello there ', 128 | expected: 'Hello there...' 129 | end 130 | 131 | describe 'single html tag elements' do 132 | it_truncates 'html text with
element without adding a closing tag', 133 | with: { max_length: 30, tail: '...' }, 134 | source: '


some text 1

some text 2

', 135 | expected: '


so...

' 136 | 137 | it_truncates 'html text with
element with a closing tag', 138 | with: { max_length: 30, tail: '...' }, 139 | source: '



some text 1

some text 2

', 140 | expected: '


so...

' 141 | 142 | it_truncates 'html text with element without adding a closing tag', 143 | with: { max_length: 45, tail: '...' }, 144 | source: "

some text 1

some text 2

", 145 | expected: "

so...

" 146 | 147 | it_truncates 'html text with element with a closing tag', 148 | with: { max_length: 45, tail: '...' }, 149 | source: "

some text 1

some text 2

", 150 | expected: "

so...

" 151 | end 152 | 153 | describe 'invalid html' do 154 | it_truncates 'html text with unclosed elements 1', 155 | with: { max_length: 30, tail: '...' }, 156 | source: '


some text 1

some text 2', 157 | expected: '


so...

' 158 | it_truncates 'html text with unclosed elements 2', 159 | with: { max_length: 30, tail: '...' }, 160 | source: '


some text 1

some text 2', 161 | expected: '


so...

' 162 | it_truncates 'html text with unclosed br element', 163 | with: { max_length: 30, tail: '...' }, 164 | source: '


some text 1

some text 2', 165 | expected: '


so...

' 166 | it_truncates 'html text with mis-matched elements', 167 | with: { max_length: 30, tail: '...' }, 168 | source: '


some text 1

some text 2', 169 | expected: '


so...

' 170 | end 171 | 172 | describe 'comment html element' do 173 | it_does_not_truncate 'retains comments', 174 | with: { max_length: 35 }, 175 | source: '
', 176 | expected: '
' 177 | 178 | it_truncates "doesn't truncate comments themselves (all or nothing)", 179 | with: { max_length: 34 }, 180 | source: '
', 181 | expected: '
' 182 | end 183 | 184 | describe 'html attributes' do 185 | it_truncates 'html text with 1 attributes', 186 | with: { max_length: 23, tail: '...' }, 187 | source: "

some text

", 188 | expected: "

som...

" 189 | 190 | it_truncates 'html text with 2 attributes', 191 | with: { max_length: 33, tail: '...' }, 192 | source: "

some text

", 193 | expected: "

som...

" 194 | 195 | it_truncates 'html text with attributes in nested tags', 196 | with: { max_length: 35, tail: '...' }, 197 | source: "

some text

", 198 | expected: "

some...

" 199 | end 200 | 201 | describe 'cdata blocks' do 202 | # This comes from a real-world example which requires cdata support to bring in the CSS 203 | let(:cdata_example) { File.read('spec/fixtures/cdata_example.html') } 204 | 205 | it 'cdata blocks are preserved' do 206 | text, truncated = Abbreviato.truncate(cdata_example, max_length: 65_535) 207 | expect(text.length).to eq 3581 208 | expect(truncated).to be_falsey 209 | end 210 | end 211 | 212 | describe 'edge-cases: long tags' do 213 | it_truncates 'completely removes tags and contents if the tags will not fit', 214 | with: { max_length: 33, tail: '...' }, 215 | source: 'text', 216 | expected: '' 217 | it_truncates 'does not allow closing tags to get added without opening tags', 218 | with: { max_length: 61, tail: '...' }, 219 | source: 'text', 220 | expected: '' 221 | it_truncates 'does not allow closing tags to get added without opening tags', 222 | with: { max_length: 62, tail: '...' }, 223 | source: 'text', 224 | expected: '...' 225 | it_does_not_truncate 'does not allow closing tags to get added without opening tags', 226 | with: { max_length: 63, tail: '...' }, 227 | source: 'text', 228 | expected: 'text' 229 | end 230 | 231 | describe 'edge-cases:' do 232 | it_truncates 'discard a single element within a longer element and use the following html (assuming it will fit)', 233 | with: { max_length: 9 }, 234 | source: '

.

', 235 | expected: '

.

' 236 | it_truncates 'discard a single element within a longer element', 237 | with: { max_length: 10 }, 238 | source: '

.

', 239 | expected: '

.

' 240 | it_truncates 'some semi-random elements', 241 | with: { max_length: 10 }, 242 | source: '





.


', 243 | expected: '

.

' 244 | it_truncates 'outer tags fit exactly', 245 | with: { max_length: 13 }, 246 | source: '

', 247 | expected: '' 248 | it_truncates 'outer tags and opening inner tag fit exactly', 249 | with: { max_length: 16 }, 250 | source: '

', 251 | expected: '' 252 | it_truncates 'void tags which do not fit', 253 | with: { max_length: 5 }, 254 | source: '', 255 | expected: '' 256 | it_truncates 'void tags within outer tags which do not fit', 257 | with: { max_length: 15 }, 258 | source: '

', 259 | expected: '

' 260 | it_does_not_truncate 'void tags within outer tags which fit', 261 | with: { max_length: 17 }, 262 | source: '

', 263 | expected: '

' 264 | it_truncates 'outer tags which fit perfectly within inner content', 265 | with: { max_length: 7 }, 266 | source: '

hello

', 267 | expected: '

' 268 | end 269 | 270 | describe 'edge-cases: junk' do 271 | it_truncates 'junk, including various html chars', 272 | with: { max_length: 10 }, 273 | source: '<<< / > < 0)(*&^*&^%${#}>hello there

', 282 | expected: '<

' 283 | it_truncates 'closing bracket only', 284 | with: { max_length: 10 }, 285 | source: '>', 286 | expected: '

' 287 | end 288 | 289 | describe 'void tags' do 290 | TruncatedSaxDocument::VOID_TAGS.each do |tag| 291 | it_does_not_truncate "void tag: #{tag} doesn't get closing element added", 292 | with: { max_length: 100 }, 293 | source: "<#{tag}/>", 294 | expected: "<#{tag}/>" 295 | 296 | it_does_not_truncate "void tag: #{tag} has closing element stripped", 297 | with: { max_length: 100 }, 298 | source: "<#{tag}>", 299 | expected: "<#{tag}/>" 300 | end 301 | end 302 | 303 | describe 'fragment mode' do 304 | it_truncates "doesn't add `head` and `body` tags", 305 | with: { max_length: 15, tail: '...' }, 306 | source: '

hello there

', 307 | expected: '

hello...

' 308 | end 309 | 310 | describe 'document mode' do 311 | it_truncates 'preserves `html`, `head` and `body` tags', 312 | with: { max_length: 54, fragment: false, tail: '...' }, 313 | source: '

hello there

', 314 | expected: '

hello...

' 315 | 316 | it_does_not_truncate 'preserves html attributes', 317 | with: { max_length: 999, fragment: false, tail: '...' }, 318 | source: "", 319 | expected: "" 320 | 321 | it_does_not_truncate 'preserves meta tags and their attributes', 322 | with: { max_length: 999, fragment: false, tail: '...' }, 323 | source: "", 324 | expected: "" 325 | 326 | it_does_not_truncate 'preserves comments in the head', 327 | with: { max_length: 999, fragment: false, tail: '...' }, 328 | source: '', 329 | expected: '' 330 | 331 | it_does_not_truncate 'preserves styles', 332 | with: { max_length: 999, fragment: false, tail: '...' }, 333 | source: '', 334 | expected: '' 335 | 336 | it_does_not_truncate 'preservers body attributes', 337 | with: { max_length: 999, fragment: false, tail: '...' }, 338 | source: "", 339 | expected: "" 340 | end 341 | 342 | describe 'wbr element support' do 343 | it_does_not_truncate 'preserves elements', 344 | with: { max_length: 100 }, 345 | source: '

The quickbrown foxjumped over the lazy dog

', 346 | expected: '

The quickbrown foxjumped over the lazy dog

' 347 | end 348 | 349 | describe 'entity conversion' do 350 | it_does_not_truncate 'converts © character into html entity', 351 | with: { max_length: 100 }, 352 | source: '

© Copyright

', 353 | expected: '

© Copyright

' 354 | it_does_not_truncate 'converts non-english characters into html entities', 355 | with: { max_length: 200 }, 356 | source: '

Ursäkta det tagit lite tid men jag väntade på krediteringen på 160 kr vilken aldrig kom (som vanligt).

', 357 | expected: '

Ursäkta det tagit lite tid men jag väntade på krediteringen på 160 kr vilken aldrig kom (som vanligt).

' 358 | end 359 | 360 | describe 'html encoded entities' do 361 | it_truncates 'html entities', 362 | with: { max_length: 18, fragment: true, tail: '...' }, 363 | source: '""""', 364 | expected: '

"...

' 365 | 366 | it_truncates 'text with html entitities', 367 | with: { max_length: 50, fragment: true, tail: '...' }, 368 | source: 'table cellpadding="0" cellspacing="0"', 369 | expected: '

table cellpadding="0" cellspa...

' 370 | end 371 | 372 | describe 'mid-row truncation' do 373 | describe 'and a well-formatted table is absent' do 374 | it_truncates 'does not attempt table truncation', 375 | with: { max_length: 120, truncate_incomplete_row: true }, 376 | source: '
Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor inci ex ea commodo consequat.
', 377 | expected: '
Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipi…
' 378 | 379 | it_truncates 'does not attempt table truncation', 380 | with: { max_length: 120, truncate_incomplete_row: true }, 381 | source: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor inci ex ea commodo consequat.
', 382 | expected: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur a…
' 383 | 384 | it_truncates 'does not attempt table truncation', 385 | with: { max_length: 120, truncate_incomplete_row: true }, 386 | source: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor inci ex ea commodo consequat.
', 387 | expected: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, cons…
' 388 | end 389 | 390 | describe 'when truncate_incomplete_row option is provided' do 391 | it_truncates 'drops the last in the document', 392 | with: { max_length: 120, truncate_incomplete_row: true }, 393 | source: '
aaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbb
ccccccccccccccccccccccdddddddddddddddddddddd
', 394 | expected: "\n\n\n
aaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbb
" 395 | end 396 | 397 | describe 'when truncate_incomplete_row option is absent' do 398 | it_truncates 'does not drop the last in the document', 399 | with: { max_length: 120, truncate_incomplete_row: false }, 400 | source: '
aaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbb
ccccccccccccccccccccccdddddddddddddddddddddd
', 401 | expected: '
aaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbb
cccccccc…
' 402 | end 403 | end 404 | 405 | # Preserve this code as an example of truncating a real-world (PII-less) example 406 | # let(:real_world_doc) { File.read('spec/fixtures/real_world_example.html') } 407 | # it "works with a real-life example" do 408 | # truncated = Abbreviato.truncate(real_world_doc, max_length: 65535) 409 | # File.open('spec/fixtures/real_world_example_truncated.html', 'w') { |file| file.write(truncated) } 410 | # end 411 | end 412 | -------------------------------------------------------------------------------- /spec/fixtures/real_world_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 147 | 148 | 149 |
150 |

John / Pete,

151 |

 

152 |

Good morning.  I am back from vacation and know there were questions regarding the quote for coolers and gasket kit while I was out.  During my absence, I had Johny Johnson forward 153 | my quote to John for his reference.

154 |

 

155 |

Are there any outstanding questions you have?  If not, when do you anticipate the order will be placed?  I want to make sure this is pushed through quickly so we meet your 156 | onsite date set forth by Janey Janeson.

157 |

 

158 |

Confirming information again:

159 |

 

160 | 161 | 162 | 163 | 166 | 169 | 172 | 175 | 176 | 177 | 180 | 183 | 186 | 189 | 190 | 191 | 194 | 197 | 200 | 203 | 204 | 205 | 208 | 211 | 214 | 217 | 218 | 219 |
164 |

Qty

165 |
167 |

PN

168 |
170 |

Net Each

171 |
173 |

Total

174 |
178 |

3

179 |
181 |

218100

182 |
184 |

2,611.00

185 |
187 |

7,833.00

188 |
192 |

1

193 |
195 |

6750346

196 |
198 |

434.00

199 |
201 |

434.00

202 |
206 |

 

207 |
209 |

 

210 |
212 |

Total:

213 |
215 |

76,267.00

216 |
220 |

 

221 |

 

222 |

Thank you and regards,

223 |

 

224 |

Katy Katyson

225 |

Order Management – Strategic Accounts

226 |

Helicopter Refitting and Services

227 |

Helicopter And Doggie Grooming Services

228 |

1000 Main Street

229 |

San Francisco, CA  94100 USA

230 |

#858584

231 |

Office:        123-456-7890

232 |

E-Fax:          1-123-456-7890 233 |

234 |

Email:          235 | linda_lindason@helidogs.com

236 |

Website:     237 | www.helidogs.com

238 |

Parts Emergencies After Hours:  123-456-7890

239 |

cid:image003.jpg@01CDF4FF.BBB74380

240 |

 

241 |

 

242 |

 

243 |

 

244 |

From: Johntopher@blah.com [mailto:Johntopher@blah.com] 245 |
246 | Sent: Thursday, August 18, 2016 11:59 AM
247 | To: Monkey, Smith
248 | Cc: Lindason, Linda
249 | Subject: RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 Gasket Kits

250 |

 

251 |

Monkey/Linda, 252 |
253 |
254 | Could we please get a firm quote on this one? Reason I ask is Pete wants to make sure he adds this part to his storeroom so the next time they need to order, he can pull this quote for refernce and 255 | order the exact same cooler.
256 |
257 | Thanks,
258 |
259 | john Johnson 260 |
261 | Regional Lead Buyer - Plant & Production 262 |
263 | Region Americas Procurement 264 |
265 | LotsOStuff Inc 266 |
267 |
268 | Someplace, USA 269 |
270 | Office: +1 234-567-7890; Fax: +1 234-567-7890 271 |
272 | Mobile: +1 234-567-7890 273 |
274 | Email: 275 | Johntopher@blah.com | www.monkeywrenches.com 276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 | From:        "Monkey, Smith" <Brady_McBunch@iiii.com> 284 |
285 | To:        "Morten.McMortensson@monkeywrenches.com" <Morten.McMortensson@monkeywrenches.com>, 286 |
287 | Cc:        "Johntopher@blah.com" <Johntopher@blah.com>, 288 | "Lindason, Linda" <linda_lindason@helidogs.com> 289 |
290 | Date:        08/16/2016 09:31 AM 291 |
292 | Subject:        RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 Gasket Kits 293 |

294 |
295 |
296 |
297 |


298 |
299 |
300 | 10-4 Pete…. Keep both Linda and I posted so we can process once received. 301 |
302 |   303 |
304 | THANKS 305 |
306 |   307 |
308 |   309 |
310 |   311 |
312 | Gibb 313 |
314 | 513-383-5019 315 |
316 |   317 |
318 | From: 319 | Morten.McMortensson@monkeywrenches.com [mailto:Morten.McMortensson@monkeywrenches.com] 320 |
321 | Sent:
Tuesday, August 16, 2016 9:29 AM
322 | To:
Monkey, Smith <Brady_McBunch@iiii.com>
323 | Cc:
Johntopher@blah.com; Lindason, Linda <linda_lindason@helidogs.com>
324 | Subject:
RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 Gasket Kits
325 |
326 |  
327 | I will get the PR in today and speak with the procurement folks about getting it expedited to you today. 328 |
329 |
330 | Murph McMurphsson

331 | LotsOStuff LLC

332 | Area Supply Manager, Washington WV
333 |
334 | 1 1st St
335 |
336 | San Francisco, CA 94100
337 |
338 | Office: 1-234-567-8900
339 |
340 | Mobile: 1-234-567-8900
341 |
342 | Fax: 1-234-567-8900

343 |
Morten.McMortensson@monkeywrenches.com 344 |
345 |
346 |
347 |
348 | From:        
"Monkey, Smith" <Brady_McBunch@iiii.com> 349 |
350 | To:        
"Lindason, Linda" <linda_lindason@helidogs.com>, 351 | "Johntopher@blah.com" <Johntopher@blah.com>, 352 | "Morten.McMortensson@monkeywrenches.com" <Morten.McMortensson@monkeywrenches.com>, 353 |
354 | Date:        
08/16/2016 09:18 AM
355 |
356 | Subject:        
RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 Gasket Kits 357 |

358 |
359 |
360 |
361 |


362 |
363 |
364 |
365 | Pete…..
366 | When do you expect the purchase order to hit?  Linda and I will try to allocate the stock for this order asap.  THANKS
367 |
368 |
 
369 |
 
370 |
 
371 | Gibb
372 |
373 | 513-383-5019
374 |
375 |
 
376 | From:
Lindason, Linda 377 |
378 | Sent:
Monday, August 15, 2016 3:28 PM
379 | To:
Johntopher@blah.com; 380 | Morten.McMortensson@monkeywrenches.com
381 | Cc:
Monkey, Smith <Brady_McBunch@iiii.com>
382 | Subject:
LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 Gasket Kits
383 |
384 |  
385 | John / Pete

386 |
 
387 | The gasket kit is part number 34534563.  Net price is 2,434.00.  Currently 3 kits in stock.
388 |
389 |  
390 | Please advise any questions, thank you.
391 |
392 |
 
393 | Regards,

394 |
 
395 | Katy Katyson
396 |
397 | Order Management – Strategic Accounts
398 |
399 | Helicopter Refitting and Services
400 |
401 | Helicopter And Doggie Grooming Services
402 |
403 | 1000 Main Street
404 |
405 | San Francisco, NC  94100 USA
406 |
407 |
 
408 | Office:        123-456-7890
409 |
410 | E-Fax:          1-123-456-7890
411 | Email:          
linda_lindason@helidogs.com 412 |
413 | Website:    
www.helidogs.com 414 |
415 | Parts Emergencies After Hours:  123-456-7890

416 | cid:image003.jpg@01CDF4FF.BBB74380
417 |
 
418 | From:
419 | Johntopher@blah.com [mailto:Johntopher@blah.com] 420 |
421 | Sent:
Monday, August 15, 2016 3:05 PM
422 | To:
Morten.McMortensson@monkeywrenches.com
423 | Cc:
Monkey, Smith <Brady_McBunch@iiii.com>; 424 | Lindason, Linda <linda_lindason@helidogs.com>
425 | Subject:
RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12

426 |  
427 | Brandon,

428 |
429 | Pete will start working on getting a work order in the system so that we do not lose any coolers that are in stock. We will order the 3.
430 |
431 |
432 | Thanks,

433 |
434 | john Johnson
435 |
436 | Regional Lead Buyer - Plant & Production
437 |
438 | Region Americas Procurement
439 |
440 | LotsOStuff Inc
441 |
442 |
443 | Someplace, USA
444 |
445 | Office: +1 234-567-7890; Fax: +1 234-567-7890
446 |
447 | Mobile: +1 234-567-7890
448 |
449 | Email:
Johntopher@blah.com | 450 | www.monkeywrenches.com 451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 | From:        
Murph McMurphsson/US/LG/LotsOStuffGroup 459 |
460 | To:        
"Monkey, Smith" <Brady_McBunch@iiii.com>, 461 |
462 | Cc:        
"
Johntopher@blah.com" <Johntopher@blah.com>, 463 | "Lindason, Linda" <linda_lindason@helidogs.com> 464 |
465 | Date:        
08/15/2016 03:02 PM 466 |
467 | Subject:        
RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 468 |

469 |
470 |
471 |
472 |


473 |
474 |
475 |
476 | I will also need the gasket kits.  I will get you the numbers for the kits.
477 |
478 |
479 | Murph McMurphsson

480 | LotsOStuff LLC

481 | Area Supply Manager, Washington WV
482 |
483 | 1 1st St
484 |
485 | San Francisco, CA 94100
486 |
487 | Office: 1-234-567-8900
488 |
489 | Mobile: 1-234-567-8900
490 |
491 | Fax: 1-234-567-8900

492 |
Morten.McMortensson@monkeywrenches.com 493 |
494 |
495 |
496 |
497 |
498 | From:        
"Monkey, Smith" <Brady_McBunch@iiii.com> 499 |
500 | To:        
"Johntopher@blah.com" 501 | <Johntopher@blah.com>, "Lindason, Linda" <linda_lindason@helidogs.com>, 502 | "Morten.McMortensson@monkeywrenches.com" <Morten.McMortensson@monkeywrenches.com>, 503 |
504 | Date:        
08/15/2016 02:43 PM
505 |
506 | Subject:        
RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 507 |

508 |
509 |
510 |
511 |


512 |
513 |
514 |
515 |
516 | Ok John,
517 |
518 | We have 7 of these in stock.  Linda & I are able to honor that March price….. .85 from list.  Your cost each = $24,611.
519 |
520 |
521 | Advise what you need.  THANKS John

522 |
523 |
524 |
525 | Gibb
526 |
527 | 513-383-5019

528 |
529 | From:
530 | Johntopher@blah.com [mailto:Johntopher@blah.com] 531 |
532 | Sent:
Monday, August 15, 2016 1:36 PM
533 | To:
Lindason, Linda <
linda_lindason@helidogs.com>; 534 | Morten.McMortensson@monkeywrenches.com
535 | Cc:
Monkey, Smith <Brady_McBunch@iiii.com>; 536 | Murphey, James <James.Murphey@iiii.com>
537 | Subject:
Fw: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12

538 |
539 | Linda,

540 | Here is the nameplate on the unit that these coolers are needed for.
541 |
542 |
543 | Pete,

544 | The length is different dependent on the 125 vs. 150.
545 |
546 |
547 | Thanks,

548 |
549 | john Johnson
550 |
551 | Regional Lead Buyer - Plant & Production
552 |
553 | Region Americas Procurement
554 |
555 | LotsOStuff Inc
556 |
557 |
558 | Someplace, USA
559 |
560 | Office: +1 234-567-7890; Fax: +1 234-567-7890
561 |
562 | Mobile: +1 234-567-7890
563 |
564 | Email:
Johntopher@blah.com | 565 | www.monkeywrenches.com 566 |
567 |
568 |
569 |
570 | ----- Forwarded by Johntopher Rolf/US/LG/LotsOStuffGroup on 08/15/2016 01:34 PM -----
571 |
572 |
573 | From:        
Murph McMurphsson/US/LG/LotsOStuffGroup 574 |
575 | To:        
Johntopher Rolf/US/LG/LotsOStuffGroup@LotsOStuffGroup, 576 |
577 | Date:        
08/15/2016 01:33 PM
578 |
579 | Subject:        
Re: Fw: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 580 |

581 |
582 |
583 |
584 |


585 |
586 |
587 |
588 |
589 | John,

590 | This is the compressor that I need them for.  From what I understand the coolers should be the same for a 100, 125 or a 150.  I could be wrong.
591 |
592 |
593 |
594 |
595 | Murph McMurphsson

596 | LotsOStuff LLC

597 | Area Supply Manager, Washington WV
598 |
599 | 1 1st St
600 |
601 | San Francisco, CA 94100
602 |
603 | Office: 1-234-567-8900
604 |
605 | Mobile: 1-234-567-8900
606 |
607 | Fax: 1-234-567-8900

608 |
Morten.McMortensson@monkeywrenches.com 609 |
610 |
611 |
612 |
613 | From:        
Johntopher Rolf/US/LG/LotsOStuffGroup 614 |
615 | To:        
Murph McMurphsson/US/LG/LotsOStuffGroup@LotsOStuffGroup, 616 |
617 | Date:        
08/15/2016 01:15 PM
618 |
619 | Subject:        
Fw: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 620 |

621 |
622 |
623 |
624 |


625 |
626 |
627 |
628 |
629 | Pete,

630 |
631 | IR is getting conflicting information. Originally, you had me quote serial# 4125. Now the updated pic is 4150. I'm not sure which you had CTI quote. What is the correct serial number you have on this unit that you need 3 of?
632 |
633 |
634 | Thanks,

635 |
636 | john Johnson
637 |
638 | Regional Lead Buyer - Plant & Production
639 |
640 | Region Americas Procurement
641 |
642 | LotsOStuff Inc
643 |
644 |
645 | Someplace, USA
646 |
647 | Office: +1 234-567-7890; Fax: +1 234-567-7890
648 |
649 | Mobile: +1 234-567-7890
650 |
651 | Email:
Johntopher@blah.com | 652 | www.monkeywrenches.com 653 |
654 |
655 |
656 |
657 | ----- Forwarded by Johntopher Rolf/US/LG/LotsOStuffGroup on 08/15/2016 01:14 PM -----
658 |
659 |
660 | From:        
"Lindason, Linda" <linda_lindason@helidogs.com> 661 |
662 | To:        
"Monkey, Smith" <Brady_McBunch@iiii.com>, 663 | "Murphey, James" <James.Murphey@iiii.com>, "'Johntopher@blah.com'" 664 | <Johntopher@blah.com>, 665 |
666 | Date:        
08/15/2016 01:09 PM
667 |
668 | Subject:        
RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 669 |

670 |
671 |
672 |
673 |


674 |
675 |
676 |
677 |
678 |
679 | Folks,

680 |
681 | I can’t quote without a serial number.  The PN quoted last time was for a 4125 and this inquiry states 4150.  This is conflicting information.
682 |
683 |
684 | Not the same cooler – length is different.  Must have SN.  
685 |
686 |
687 | Time is critical so I can get booked and expedite delivery in time to meet the requested date NLT 10/10.
688 |
689 |
690 | Regards,

691 |
692 | Katy Katyson
693 |
694 | Order Management – Strategic Accounts
695 |
696 | Helicopter Refitting and Services
697 |
698 | Helicopter And Doggie Grooming Services
699 |
700 | 1000 Main Street
701 |
702 | San Francisco, NC  94100 USA
703 |
704 |
705 | Office:        123-456-7890
706 |
707 | E-Fax:          1-123-456-7890
708 | Email:          
linda_lindason@helidogs.com 709 |
710 | Website:    
www.helidogs.com 711 |
712 | Parts Emergencies After Hours:  123-456-7890

713 | cid:image003.jpg@01CDF4FF.BBB74380
714 |
715 | From:
Monkey, Smith 716 |
717 | Sent:
Monday, August 15, 2016 1:05 PM
718 | To:
Lindason, Linda <
linda_lindason@helidogs.com>; Murphey, James <James.Murphey@iiii.com>
719 | Subject:
Fwd: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12

720 |
721 | Linda..... See below.  Still stock?
722 |
723 | GIBB
724 | Sent from phone
725 |
726 | Begin forwarded message:
727 | From:
<Johntopher@blah.com>
728 | Date:
August 15, 2016 at 1:03:22 PM EDT
729 | To:
"Monkey, Smith" <Brady_McBunch@iiii.com>
730 | Subject:
RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 731 |
732 | Brandon,

733 |
734 | These are the same ones we worked on previously. A couple questions:
735 |

736 |
    737 |
  • 738 | Are there still 3 in stock? 739 |
  • 740 | Can we discuss pricing? It would be beneficial if we can go lower than the .85 multiplier. 741 |
  • 742 |  
743 |

Thanks, 744 |
745 |
746 | john Johnson
747 |
748 | Regional Lead Buyer - Plant & Production
749 |
750 | Region Americas Procurement
751 |
752 | LotsOStuff Inc
753 |
754 |
755 | Someplace, USA
756 |
757 | Office: +1 234-567-7890; Fax: +1 234-567-7890
758 |
759 | Mobile: +1 234-567-7890
760 |
761 | Email:
Johntopher@blah.com | 762 | www.monkeywrenches.com 763 |
764 |
765 |
766 |
767 |
768 |
769 |
770 | From:        
"Monkey, Smith" <Brady_McBunch@iiii.com> 771 |
772 | To:        
"Lindason, Linda" <linda_lindason@helidogs.com>, 773 | "Johntopher@blah.com" <Johntopher@blah.com>, 774 |
775 | Date:        
08/15/2016 09:31 AM
776 |
777 | Subject:        
RE: LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 778 |

779 |
780 |
781 |
782 |


783 |
784 |
785 |
786 |
787 |
788 |
789 | John,
790 |
791 | Here is the request and Linda’s followup request for serial number.  This cooler has gone through some revisions.  We are attempting to make sure we quote the correct one.
792 |
793 |
794 | We want to work with you and the site on this….. advise how we can best do this.  Thank you John.
795 |
796 |
797 |
798 |
799 | Gibb
800 |
801 | 513-383-5019

802 |
803 | From:
Lindason, Linda 804 |
805 | Sent:
Friday, August 12, 2016 9:32 AM
806 | To:
Morten.McMortensson@monkeywrenches.com; 'johanna.ramirez@monkeywrenches.com' 807 | <johanna.ramirez@monkeywrenches.com>
808 | Cc:
ITS_E-COM_ADMIN, IR <
its_e-com_admin@iiii.com>; Monkey, Smith 809 | <Brady_McBunch@iiii.com>
810 | Subject:
LotsOStuff: ***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12
811 |
812 |
813 | Pete,

814 |
815 | Good morning,  Confirming discussion ,please provide the serial number of the compressor so we can confirm correct part number for your build.  
816 |
817 |
818 | Thank you,
819 |
820 |
821 |
822 | Regards,

823 |
824 | Katy Katyson
825 |
826 | Order Management – Strategic Accounts
827 |
828 | Helicopter Refitting and Services
829 |
830 | Helicopter And Doggie Grooming Services
831 |
832 | 1000 Main Street
833 |
834 | San Francisco, NC  94100 USA
835 |
836 |
837 | Office:        123-456-7890
838 |
839 | E-Fax:          1-123-456-7890
840 | Email:          
linda_lindason@helidogs.com 841 |
842 | Website:    
www.helidogs.com 843 |
844 | Parts Emergencies After Hours:  123-456-7890

845 | cid:image003.jpg@01CDF4FF.BBB74380
846 | #744794

847 |
848 | From:
849 | johanna.ramirez@monkeywrenches.com [mailto:johanna.ramirez@monkeywrenches.com] 850 |
851 | Sent:
Friday, August 12, 2016 9:08 AM
852 | Cc:
matthew.friel@monkeywrenches.com
853 | Subject:
***CRITICAL*** RFQ COOLER WEST VIRGINIA 08/12 854 |
855 |
856 | Dear LotsOStuff Vendor,
857 |
858 | I will need a quote for  three coolers  need the material on site by  10th of October, but the sooner I could get them the better.
859 |
860 |
861 |
862 |
863 | PLEASE CONFIRM EMAIL RECEIVED.

864 |
865 | For Technical information please contact
866 |
867 | Murph McMurphsson

868 | LotsOStuff LLC

869 | Area Supply Manager, Washington WV
870 |
871 | 1 1st St
872 |
873 | San Francisco, CA 94100
874 |
875 | Office: 1-234-567-8900
876 |
877 | Mobile: 1-234-567-8900
878 |
879 | Fax: 1-234-567-8900

880 |
Morten.McMortensson@monkeywrenches.com 881 |
882 |
883 |
884 | Please keep me on a copy for the quote matters and commercial terms.
885 |
886 | Thank you!

887 | Johanna Ramirez
888 |
889 | Buyer – Plant & Production
890 |
891 | Procurement Execution
892 |
893 | Butler, PA
- US HUB
894 | LotsOStuff North America Inc
895 |
896 |
897 | 1 1st Ave, San Francisco, CA 94111
898 |
899 | Phone + 724-285-2003 ; Fax +724-285-5559

900 |

 

901 |
902 |
903 |
904 |


905 |
906 |
907 |
908 |
909 | The information contained in this message is privileged and intended only for the recipients named. If the reader is not a representative of the intended recipient, any review, dissemination or copying of this message or the information it contains is prohibited. 910 | If you have received this message in error, please immediately notify the sender, and delete the original message and attachments.

911 |
912 |
913 | Caution:
The identity of the sender of this message could not be verified as it originates from the Internet. Please pay attention. 914 |

915 |

 

916 |
917 |
918 |
919 |


920 |
921 |
922 |
923 | The information contained in this message is privileged and intended only for the recipients named. If the reader is not a representative of the intended recipient, any review, dissemination or copying of this message or the information it contains is prohibited. 924 | If you have received this message in error, please immediately notify the sender, and delete the original message and attachments.

925 |
926 |
927 | Caution:
The identity of the sender of this message could not be verified as it originates from the Internet. Please pay attention. 928 |

929 |

 

930 |
931 |
932 |
933 |


934 |
935 |
936 | The information contained in this message is privileged and intended only for the recipients named. If the reader is not a representative of the intended recipient, any review, dissemination or copying of this message or the information it contains is prohibited. 937 | If you have received this message in error, please immediately notify the sender, and delete the original message and attachments.

938 |
939 |
940 | Caution:
The identity of the sender of this message could not be verified as it originates from the Internet. Please pay attention. 941 |

942 |

 

943 |
944 |
945 |
946 |


947 |
948 | The information contained in this message is privileged and intended only for the recipients named. If the reader is not a representative of the intended recipient, any review, dissemination or copying of this message or the information it contains is prohibited. 949 | If you have received this message in error, please immediately notify the sender, and delete the original message and attachments.

950 |
951 |
952 | Caution:
The identity of the sender of this message could not be verified as it originates from the Internet. Please pay attention.
953 |

954 |

 

955 |
956 |
957 |
958 |


959 | The information contained in this message is privileged and intended only for the recipients named. If the reader is not a representative of the intended recipient, any review, dissemination or copying of this message or the information it contains is prohibited. 960 | If you have received this message in error, please immediately notify the sender, and delete the original message and attachments.

961 |
962 |
963 | Caution:
The identity of the sender of this message could not be verified as it originates from the Internet. Please pay attention.
964 |

965 |
966 |
967 |
968 |
969 | The information contained in this message is privileged and intended only for the recipients named. If the reader is not a representative of the intended recipient, any review, dissemination or copying of this message or the information it contains is prohibited. 970 | If you have received this message in error, please immediately notify the sender, and delete the original message and attachments.
971 |
972 | 973 | --------------------------------------------------------------------------------