Угловое связывание HTML


842

Я пишу приложение Angular, и у меня есть ответ HTML, который я хочу отобразить.

Как я могу это сделать? Если я просто использую синтаксис привязки, {{myVal}}он кодирует все символы HTML (конечно).

Мне нужно как-то привязать innerHTMLa divк значению переменной.


Связанный пост о том, как определить CSS в компоненте для правильной работы в HTML-привязке stackoverflow.com/questions/36265026/…
y3sh

1
Я собрал видеоответ, чтобы объяснить решение и привести пример: youtube.com/watch?v=Pem2UXp7TXA
Caleb Grams

Что, если переменная содержит угловой тег или пользовательский тег, например, <span [routerLink] = ['some-router']> link </ span>
G. Muqtada

Ответы:


1340

Правильный синтаксис следующий:

<div [innerHTML]="theHtmlString"></div>

Справочная документация



14
Есть ли способ, которым я могу заставить angular запускать его привязку к элементам innerHTML? Мне нужно использовать <a [router-link]="..."> </a>, и я хочу предоставить это из внешнего html.
thouliha

4
@thouliha Я бы порекомендовал начать новый пост по вашему вопросу.
prolink007

4
Он отображает строку в моем случае, но что-то делает с разметкой. Кажется, удалили атрибуты в разметке. Я на 2.4.6
пакогомез

2
@paqogomez Да, он удаляет все, что он считает небезопасным
Хуан Мендес

313

Angular 2.0.0 и Angular 4.0.0 final

Для безопасного контента просто

<div [innerHTML]="myVal"></div>

DOMSanitizer

Потенциально небезопасный HTML должен быть явно помечен как надежный с помощью средства гигиены Angulars DOM, чтобы не удалять потенциально небезопасные части содержимого

<div [innerHTML]="myVal | safeHtml"></div>

с трубкой как

@Pipe({name: 'safeHtml'})
export class Safe {
  constructor(private sanitizer:DomSanitizer){}

  transform(style) {
    return this.sanitizer.bypassSecurityTrustHtml(style);
    //return this.sanitizer.bypassSecurityTrustStyle(style);
    // return this.sanitizer.bypassSecurityTrustXxx(style); - see docs
  }
}

См. Также В RC.1 некоторые стили не могут быть добавлены с использованием синтаксиса привязки.

И документы: https://angular.io/api/platform-browser/DomSanitizer

Предупреждение безопасности

Доверие к добавленному пользователем HTML может представлять угрозу безопасности. В упомянутых выше документах говорится :

Вызов любого из bypassSecurityTrust...API отключает встроенную очистку Angular для переданного значения. Тщательно проверяйте и проверяйте все значения и пути кода, входящие в этот вызов. Убедитесь, что любые пользовательские данные надлежащим образом экранированы для этого контекста безопасности. Для получения более подробной информации см. Руководство по безопасности .

Угловая разметка

Что-то вроде

class FooComponent {
  bar = 'bar';
  foo = `<div>{{bar}}</div>
    <my-comp></my-comp>
    <input [(ngModel)]="bar">`;

с

<div [innerHTML]="foo"></div>

не заставит Angular обрабатывать что-либо специфичное для Angularfoo . Angular заменяет определенную разметку Angular во время сборки на сгенерированный код. Разметка, добавленная во время выполнения , не будет обрабатываться Angular .

Чтобы добавить HTML-код, содержащий разметку, специфичную для Angular (привязка свойства или значения, компоненты, директивы, каналы, ...), необходимо добавить динамический модуль и скомпилировать компоненты во время выполнения. Этот ответ содержит более подробную информацию. Как я могу использовать / создать динамический шаблон для компиляции динамического компонента с Angular 2.0?


13
Это должно быть ответом. Обратите внимание на две строки, которые закомментированы. На самом деле это второй, который обрабатывает HTML.
Пакогомес

8
обязательноimport { BrowserModule, DomSanitizer } from '@angular/platform-browser'
paqogomez

4
Такжеimport { Pipe } from '@angular/core'
Аппулус

1
Это ответ, прямо здесь! Искал подробности о том, что в NG2 заменили $ SCE NG1. ;)
jrista

2
Отличный ответ. Решил мою проблему. Большое спасибо. В случае, если кто-то не уверен, как использовать канал в компоненте (как я): angular.io/guide/pipes Просто добавьте его в свои объявления в соответствующем модуле и вуаля!
Алехандро Надь

169

[innerHtml] Это отличный вариант в большинстве случаев, но он терпит неудачу с очень большими строками или когда вам нужно жестко запрограммировать стили в html.

Я хотел бы поделиться другим подходом:

Все, что вам нужно сделать, это создать div в вашем html-файле и присвоить ему некоторый идентификатор:

<div #dataContainer></div>

Затем в вашем компоненте Angular 2 создайте ссылку на этот объект (здесь TypeScript):

import { Component, ViewChild, ElementRef } from '@angular/core';

@Component({
    templateUrl: "some html file"
})
export class MainPageComponent {

    @ViewChild('dataContainer') dataContainer: ElementRef;

    loadData(data) {
        this.dataContainer.nativeElement.innerHTML = data;
    }
}

Затем просто используйте loadDataфункцию, чтобы добавить текст в элемент HTML.

Это просто способ сделать это, используя нативный JavaScript, но в среде Angular. Я не рекомендую этого, потому что делает код более запутанным, но иногда нет другого выбора.

Смотрите также Angular 2 - innerHTML style


1
Я не вижу разницы с другими решениями за исключением того, что ваш доступ к свойствам nativeElementнапрямую, что считается плохой практикой. Я уверен, что [innerHTML]="..."делает то же самое под капотом, но в хорошей практике Angular2.
Гюнтер Цохбауэр

1
Это не то, как работает Angular2. HTML, который вы добавляете в шаблоны компонентов Angular2, сначала обрабатывается Angular, а затем добавляется в DOM. У вас действительно были проблемы с [innerHTML]большими строками в Angular2?
Гюнтер Цохбауэр

1
Я думаю, что это должно быть сообщено как ошибка. Спасибо за публикацию вашего результата.
Гюнтер Цохбауэр

25
[innerHtml]удаляет стили, жестко запрограммированные в HTML. Чтобы интегрировать редактор wysiwyg, мне пришлось использовать подход, перечисленный здесь.
Джони Адамит

2
Для меня это решение работало для включения встроенного документа SVG, а [innerHTML]подход - нет.
Джаред Фелпс

54

На angular2@2.0.0-alpha.44:

Привязка Html не будет работать при использовании {{interpolation}}, вместо этого используйте «Выражение»:

недействительным

<p [innerHTML]="{{item.anleser}}"></p>

-> выдает ошибку (интерполяция вместо ожидаемого выражения)

правильный

<p [innerHTML]="item.anleser"></p>

-> Это правильный путь.

Вы можете добавить дополнительные элементы к выражению, например:

<p [innerHTML]="'<b>'+item.anleser+'</b>'"></p>

намек

HTML, добавленный с использованием [innerHTML](или добавленный динамически с помощью других средств, например element.appenChild()или аналогичных), не будет обрабатываться Angular каким-либо образом, кроме очистки в целях безопасности.
Такие вещи работают только тогда, когда HTML статически добавляется в шаблон компонентов. Если вам это нужно, вы можете создать компонент во время выполнения, как описано в разделе Как использовать / создать динамический шаблон для компиляции динамического компонента с Angular 2.0?


1
Отредактировано после попытки снова. Решение найдено :)
Jvoigt

3
Третий пример не работает. Выражение не оценить. Вывод - просто строка ... Любой другой способ объединить доверенный HTML с другими элементами тегов?
Кевин Вилела Пинто

25

Непосредственное использование [innerHTML] без использования дезинфицирующего средства DOM от Angular не подходит, если оно содержит контент, созданный пользователем. Канал safeHtml, предложенный @ GünterZöchbauer в своем ответе, является одним из способов дезинфекции контента. Следующая директива является еще одной:

import { Directive, ElementRef, Input, OnChanges, Sanitizer, SecurityContext,
  SimpleChanges } from '@angular/core';

// Sets the element's innerHTML to a sanitized version of [safeHtml]
@Directive({ selector: '[safeHtml]' })
export class HtmlDirective implements OnChanges {
  @Input() safeHtml: string;

  constructor(private elementRef: ElementRef, private sanitizer: Sanitizer) {}

  ngOnChanges(changes: SimpleChanges): any {
    if ('safeHtml' in changes) {
      this.elementRef.nativeElement.innerHTML =
        this.sanitizer.sanitize(SecurityContext.HTML, this.safeHtml);
    }
  }
}

Использоваться

<div [safeHtml]="myVal"></div>

Я пытался использовать это, но получаю следующую ошибку Can't bind to 'safeHtml' since it isn't a known property of 'div'. ng-version 2.4.4
LearnToday

1
@ObasiObenyOj вы все равно можете сделать это без использования отдельной директивы, если это ограниченный случай, constructor( private sanitizer: Sanitizer) {} и связать результат с тем, что вам нужно, также использование ElementRef настоятельно не рекомендуется.
Вейл Стив

22

Пожалуйста, обратитесь к другим ответам, которые являются более актуальными.

Это работает для меня: <div innerHTML = "{{ myVal }}"></div>(Angular2, Alpha 33)

Согласно другому SO: Вставка HTML с сервера в DOM с помощью angular2 (общие манипуляции с DOM в Angular2) , «inner-html» эквивалентна «ng-bind-html» в Angular 1.X


Правильный путь - без {{}}: <div innerHTML = "myVal"> </ div>
Кристиан Бенселер

2
Используйте [свойство] связывание синтаксиса вместо {{}} интерполяции
superluminary

Это определенно неправильный способ сделать это, и его следует отвергнуть. Это сделает все ваши html внутри атрибута div!
AsGoodAsItGets

11

Просто для полного ответа, если ваш HTML-контент находится в переменной компонента, вы также можете использовать:

<div [innerHTML]=componementVariableThatHasTheHtml></div>

10

Я прошу прощения, если я упускаю суть здесь, но я хотел бы рекомендовать другой подход:

Я думаю, что лучше возвращать необработанные данные из вашего серверного приложения и связывать их с шаблоном на стороне клиента. Это делает более гибкие запросы, поскольку вы возвращаете только json с вашего сервера.

Мне кажется, не имеет смысла использовать Angular, если все, что вы делаете, это извлекаете html с сервера и внедряете его «как есть» в DOM.

Я знаю, что Angular 1.x имеет привязку html, но я еще не видел аналога в Angular 2.0. Они могут добавить это позже, хотя. В любом случае, я бы по-прежнему рассмотрел API данных для вашего приложения Angular 2.0.

У меня есть несколько примеров с простой привязкой данных, если вы заинтересованы: http://www.syntaxsuccess.com/viewarticle/angular-2.0-examples


28
Определенно есть случаи, когда вы хотите получить и отобразить необработанный HTML. Например, получение отформатированной части статьи с пульта.
Александр Чен

2
Другой часто игнорируемый сценарий защищает бизнес-логику в шаблоне, иногда вы не хотите, чтобы неавторизованные пользователи видели логику, которую вы используете для отображения информации, поэтому вам лучше подготовить представление на стороне сервера
Ayyash

2
Также, например, отображение электронного письма в формате HTML - правда, вопрос!
Дарен

2
Если вы упускаете суть (по вашему собственному признанию), то зачем публиковать ответ? Очевидно, что цель Angular - использовать механизм представления для связывания и рендеринга данных. Но учитывая тот факт, что существует бесчисленное множество приложений, в которых может использоваться приложение Angular, фактически возможно, что одно или два из них могут иметь требование о том, что некоторые данные, которые должны отображаться в вашем приложении, уже могут быть отформатированы в HTML, и может случиться так, что разработчик не сможет контролировать этот контент. Другими словами ... актуальный вопрос.
Грегор

Да, мы должны просто отказаться от Angular и использовать JQuery, потому что Jquery делает это лучше ...
Simon_Weaver

9

Здесь уже был дан короткий ответ: используйте <div [innerHTML]="yourHtml">привязку.

Однако остальные советы, упомянутые здесь, могут вводить в заблуждение. Angular имеет встроенный механизм очистки, когда вы привязываете к таким свойствам. Так как Angular не является специальной библиотекой для очистки, он слишком усердно относится к подозрительному контенту, чтобы не рисковать. Например, он очищает все содержимое SVG в пустую строку.

Вы можете услышать советы, чтобы «дезинфицировать» ваш контент, используя методы, DomSanitizerчтобы пометить контент как безопасный с помощью bypassSecurityTrustXXXметодов. Есть также предложения использовать pipe для этого, и этот канал часто называют safeHtml.

Все это вводит в заблуждение, потому что на самом деле обходит санитарную обработку , а не санитарную обработку вашего контента. Это может быть проблемой безопасности, потому что если вы когда-либо делаете это на предоставленном пользователем контенте или на чем-то, в чем вы не уверены - вы открываете себя для атак злонамеренного кода.

Если Angular удаляет то, что вам нужно, с помощью встроенной очистки - то, что вы можете сделать вместо ее отключения, - делегировать фактическую очистку выделенной библиотеке, которая хороша для этой задачи. Например - DOMPurify.

Я сделал для него библиотеку-обертку, чтобы ее можно было легко использовать с Angular: https://github.com/TinkoffCreditSystems/ng-dompurify

У него также есть канал для декларативной очистки HTML:

<div [innerHtml]="value | dompurify"></div>

Отличие от предложенных здесь каналов заключается в том, что он действительно выполняет очистку с помощью DOMPurify и, следовательно, работает для SVG.

Следует помнить, что DOMPurify отлично подходит для дезинфекции HTML / SVG, но не CSS. Таким образом, вы можете предоставить CSS-дезинфицирующее средство Angular для обработки CSS:

import {NgModule, ɵ_sanitizeStyle} from '@angular/core';
import {SANITIZE_STYLE} from '@tinkoff/ng-dompurify';

@NgModule({
    // ...
    providers: [
        {
            provide: SANITIZE_STYLE,
            useValue: ɵ_sanitizeStyle,
        },
    ],
    // ...
})
export class AppModule {}

Он внутренний - ɵпрефикс hense , но так или иначе, команда Angular использует его и в своих собственных пакетах. Эта библиотека также работает для Angular Universal и среды перерисовки на стороне сервера.


5

Просто используйте [innerHTML]атрибут в вашем HTML , как показано ниже:

<div [innerHTML]="myVal"></div>

У вас когда-нибудь были свойства в вашем компоненте, которые содержат некоторую HTML-разметку или объекты, которые вам нужно отображать в вашем шаблоне? Традиционная интерполяция не сработает, но на помощь приходит привязка свойства innerHTML.

Использование {{myVal}} НЕ работает, как ожидалось! Это не будет брать HTML-теги, такие как <p>и <strong>т. Д., И передавать их только как строки ...

Представьте, что у вас есть этот код в вашем компоненте:

const myVal:string ='<strong>Stackoverflow</strong> is <em>helpful!</em>'

Если вы используете {{myVal}}, вы получите это в представлении:

<strong>Stackoverflow</strong> is <em>helpful!</em>

но использование [innerHTML]="myVal"делает результат, как ожидалось, вот так:

Stackoverflow это полезно!


Как бы вы отобразили свой контент, если хотите, чтобы он не содержал контейнерный div? Это возможно?
Ε Г И І И О

3

Вы можете применить несколько каналов для стиля, ссылки и HTML, как показано в .html

<div [innerHTML]="announcementContent | safeUrl| safeHtml">
                    </div>

и в трубе .ts для дезинфицирующего средства 'URL'

import { Component, Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';

@Pipe({ name: 'safeUrl' })
export class SafeUrlPipe implements PipeTransform {
    constructor(private sanitizer: DomSanitizer) {}
    transform(url) {
        return this.sanitizer.bypassSecurityTrustResourceUrl(url);
    }
}

труба для дезинфицирующего средства «HTML»

import { Component, Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';

@Pipe({
    name: 'safeHtml'
})
export class SafeHtmlPipe implements PipeTransform {
    constructor(private sanitized: DomSanitizer) {}
    transform(value) {
        return this.sanitized.bypassSecurityTrustHtml(value);
    }
}

это будет применяться как без нарушения какого-либо стиля, так и события щелчка ссылки


2

 <div [innerHTML]="HtmlPrint"></div><br>

InnerHtml это свойство HTML-элементов, что позволяет установить его HTML-содержимое программно. Существует также свойство innerText, которое определяет содержимое как простой текст.

[attributeName]="value"Коробки кронштейн, окружающий атрибут определяет угловую ввод-связывание. Это означает, что значение свойства (в вашем случае innerHtml) привязано к данному выражению, когда выражение-результат изменяется, значение свойства также изменяется.

Таким образом, в основном [innerHtml]позволяет связывать и динамически изменять html-содержимое данного HTML-элемента.


1

В Angular 2 вы можете делать 3 вида привязок:

  • [property]="expression"-> Любое свойство html может ссылаться на
    выражение. В этом случае, если выражение изменится, свойство будет обновлено, но это не сработает.
  • (event)="expression" -> Когда событие активирует выполнение выражения.
  • [(ngModel)]="property"-> Связывает свойство из js (или ts) с html. Любое обновление этого свойства будет заметно везде.

Выражение может быть значением, атрибутом или методом. Например: «4», «controller.var», «getValue ()»

Пример здесь


0

Способ динамического добавления элементов в DOM, как описано в документе Angular 2, заключается в использовании класса ViewContainerRef из @ Angular / core.

Что вам нужно сделать, это объявить директиву, которая будет реализовывать ViewContainerRef и действовать как заполнитель в вашей DOM.

директива

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appInject]'
})
export class InjectDirective {

  constructor(public viewContainerRef: ViewContainerRef) { }

}

Затем в шаблоне, куда вы хотите добавить компонент:

HTML

<div class="where_you_want_to_inject">    
  <ng-template appInject></ng-template>
</div>

Затем из введенного кода компонента вы добавите компонент, содержащий нужный HTML-код:

import { Component, OnInit, ViewChild, ComponentFactoryResolver } from '@angular/core';
import { InjectDirective } from '../inject.directive';
import { InjectedComponent } from '../injected/injected.component';

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent implements OnInit {

  @ViewChild(InjectDirective) injectComp: InjectDirective;

  constructor(private _componentFactoryResolver: ComponentFactoryResolver) {
  }

  ngOnInit() {
  }

  public addComp() {
    const componentFactory = this._componentFactoryResolver.resolveComponentFactory(InjectedComponent);
    const viewContainerRef = this.injectComp.viewContainerRef;
    const componentRef = viewContainerRef.createComponent(componentFactory);
  }

  public removeComp() {
    const componentFactory = this._componentFactoryResolver.resolveComponentFactory(InjectedComponent);
    const viewContainerRef = this.injectComp.viewContainerRef;
    const componentRef = viewContainerRef.remove();
  }

}

Я добавил полностью работающее демо-приложение на Angular 2, динамически добавив компонент в DOM demo


0

Вы можете использовать несколько подходов для достижения решения. Как уже сказано в утвержденном ответе, вы можете использовать:

<div [innerHTML]="myVal"></div>

в зависимости от того, чего вы пытаетесь достичь, вы также можете попробовать другие вещи, такие как JavaScript DOM (не рекомендуется, операции DOM медленные):

презентация

<div id="test"></test>

Составная часть

var p = document.getElementsById("test");
p.outerHTML = myVal;

Привязка недвижимости

Javascript DOM Внешний HTML


Независимо от того, выполняются ли операции DOM медленнее, чем угловые, выполнение этого с помощью getElementsByIdкакого-либо другого метода выбора является плохим, поскольку оно может захватывать элементы, принадлежащие совершенно разным компонентам, если они содержат элементы с одинаковым идентификатором (или другими критериями).
Авиад П.

Кроме того, он полностью работает за пределами любой угловой зоны, поэтому изменения не будут приняты.
Филипп Мейснер

0

Мы всегда можем передать html-контент в innerHTMLсвойство, чтобы отобразить html-динамический контент, но этот динамический html-контент также может быть зараженным или вредоносным. Поэтому, прежде чем передавать динамический контент innerHTMLнам, мы всегда должны убедиться, что контент очищен (используется DOMSanitizer), чтобы мы могли избежать всего вредоносного контента.

Попробуйте ниже трубы:

import { Pipe, PipeTransform } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";

@Pipe({name: 'safeHtml'})
export class SafeHtmlPipe implements PipeTransform {
    constructor(private sanitized: DomSanitizer) {
    }
    transform(value: string) {
        return this.sanitized.bypassSecurityTrustHtml(value);
    }
}

Usage:
<div [innerHTML]="content | safeHtml"></div>

Это необходимо, даже если вы думаете, что это не так. Например, style: background-colorвсе вещи могут быть удалены, поэтому лучше просто начать использовать это с самого начала, иначе вы очень запутаетесь.
Simon_Weaver

Что я понимаю, так это то, что этот скрипт разрешает весь вредоносный контент (bypassSecurityTrustHtml), так как вы указываете это, я думаю, что этот канал не требуется, если вы не доверяете источнику. Referto: angular.io/api/platform-browser/DomSanitizer#security-risk
Sae

0

Если вы хотите это в Angular 2 или Angular 4, а также хотите сохранить встроенный CSS, тогда вы можете использовать

<div [innerHTML]="theHtmlString | keepHtml"></div>

Это дало мне ошибку `неперехваченным (обещанию): Ошибка: Шаблон ошибки синтаксического анализа: Труба«keepHtml»не может быть found`
Praveen

import {Pipe, PipeTransform} из "@ angular / core";
Джей Момая



-2

Если у вас есть шаблоны в вашем угловом (или любом другом фреймворковом) приложении, и вы возвращаете шаблоны HTML из своего бэкэнда через HTTP-запрос / ответ, вы смешиваете шаблоны между внешним интерфейсом и бэкэндом.

Почему бы просто не оставить шаблонные материалы либо во внешнем интерфейсе (я бы это предложил), либо во внутреннем интерфейсе (довольно непрозрачный imo)?

И если вы храните шаблоны во внешнем интерфейсе, почему бы просто не отвечать JSON на запросы к внутреннему интерфейсу. Вам даже не нужно реализовывать структуру RESTful, но хранение шаблонов с одной стороны делает ваш код более прозрачным.

Это окупится, когда кто-то другой должен справиться с вашим кодом (или даже вы сами заново вводите свой код через некоторое время)!

Если вы все сделаете правильно, у вас будут небольшие компоненты с небольшими шаблонами, и, что лучше всего, если ваш код imba, тот, кто не знает языков программирования, сможет понять ваши шаблоны и вашу логику! Кроме того, сохраняйте ваши функции / методы как можно меньше. В конечном итоге вы обнаружите, что обслуживание, рефакторинг, просмотр и добавление функций будет намного проще по сравнению с большими функциями / методами / классами и смешением шаблонов и логики между внешним интерфейсом и бэкэндом - и сохраните столько же логики в бэкэнде если ваш внешний интерфейс должен быть более гибким (например, написание внешнего интерфейса Android или переключение на другой каркас внешнего интерфейса).

Философия, мужик :)

PS: вам не нужно реализовывать 100% чистый код, потому что это очень дорого - особенно если вам нужно мотивировать членов команды;) но: вы должны найти хороший баланс между подходом к более чистому коду и тем, что у вас есть (может быть, это уже довольно чисто)

проверьте книгу, если можете, и дайте ей войти в вашу душу: https://de.wikipedia.org/wiki/Clean_Code


Иногда необходимо получить HTML со стороны сервера, когда вы имеете дело со старым API, как в SOAP. Я работал над одним моим проектом с BSC (Бхаратской фондовой биржей), и они возвращали HTML-код банковской страницы при осуществлении платежей. Таким образом, вы не можете изменить их API, вы соответственно обновили свой код.
Махендра Уэйкос

Вы можете написать промежуточное программное обеспечение, которое часто запрашивает мыльный API, и предоставлять извлеченные результаты, например, в сокете. Потребление и извлечение информации с помощью мыла может быть болезненным.
Гунтрам

Действительно очевидный пример использования необработанной разметки - это когда разметка исходит из CMS и написана в редакторе WYSIWYG. Например, вы можете обслуживать несколько конечных точек из автономной системы управления контентом. Именно из-за этого у каждого движка шаблонов есть опция необработанной разметки.
Гбертон
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.